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

Better more complete fix for allowing filename to set date and slug #3762

Closed
Closed
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
2 changes: 2 additions & 0 deletions docs/content/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ theme = ""
title = ""
# if true, use /filename.html instead of /filename/
uglyURLs = false
# use dates in filenames e.g. 2017-01-31-mypostname.md
useFilenameDateAsFallback = true
# verbose output
verbose = false
# verbose logging
Expand Down
8 changes: 7 additions & 1 deletion docs/content/variables/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ See [`.Scratch`](/functions/scratch/) for page-scoped, writable variables.
: the data specific to this type of page.

`.Date`
: the date associated with the page; `.Date` pulls from the `date` field in a content's front matter. See also `.ExpiryDate`, `.PublishDate`, and `.Lastmod`.
: the date associated with the page; `.Date` pulls from the `date` field in a content's front matter.
If no `date` field present Hugo will try and extract the date from the filename. For example `2017-01-31-myblog.md`. It will also set `.Slug` to contain the path without the
date prefix.'

You can disable the automatic date detection and slug modification by setting `useFilenameDateAsFallback` configuration option to false.

See also `.ExpiryDate`, `.PublishDate`, and `.Lastmod`.

`.Description`
: the description for the page.
Expand Down
1 change: 1 addition & 0 deletions hugolib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ func loadDefaultSettingsFor(v *viper.Viper) {
v.SetDefault("enableEmoji", false)
v.SetDefault("pygmentsCodeFencesGuessSyntax", false)
v.SetDefault("useModTimeAsFallback", false)
v.SetDefault("useFilenameDateAsFallback", true)
v.SetDefault("defaultContentLanguage", "en")
v.SetDefault("defaultContentLanguageInSubdir", false)
v.SetDefault("enableMissingTranslationPlaceholders", false)
Expand Down
71 changes: 69 additions & 2 deletions hugolib/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ import (
var (
cjk = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`)

// date expression used to find iso date in filename
dateExp = regexp.MustCompile("(?P<year>\\d{4})\\-(?P<month>\\d{2})\\-(?P<day>\\d{2})-(?P<slug>.*)\\..*")

// This is all the kinds we can expect to find in .Site.Pages.
allKindsInPages = []string{KindPage, KindHome, KindSection, KindTaxonomy, KindTaxonomyTerm}

Expand Down Expand Up @@ -963,7 +966,9 @@ func (p *Page) initURLs() error {

var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter")

func (p *Page) update(f interface{}) error {
// should just read metadata - should not handle any default fallback that
// is done in updateMetadata
func (p *Page) readMetadata(f interface{}) error {
if f == nil {
return errors.New("no metadata found")
}
Expand Down Expand Up @@ -1147,13 +1152,26 @@ func (p *Page) update(f interface{}) error {
p.Params["draft"] = p.Draft

if p.Date.IsZero() && p.s.Cfg.GetBool("useModTimeAsFallback") {

fi, err := p.s.Fs.Source.Stat(filepath.Join(p.s.PathSpec.AbsPathify(p.s.Cfg.GetString("contentDir")), p.File.Path()))
if err == nil {
p.s.Log.DEBUG.Printf("using file modification time as fallback for page %s", p.File.Path())
p.Date = fi.ModTime()
p.Params["date"] = p.Date
}
}

if p.Date.IsZero() && p.s.Cfg.GetString("filenameDateFallbackPattern") != "" {
dateExp := regexp.MustCompile(p.s.Cfg.GetString("filenameDateFallbackPattern"))
dateString := dateExp.FindString(p.File.Path())
filenameDate, err := time.Parse(p.s.Cfg.GetString("filenameDateFallbackFormat"), dateString)
if err == nil {
p.s.Log.DEBUG.Printf("using filename date as fallback for page %s", p.File.Path())
p.Date = filenameDate
p.Params["date"] = p.Date
}
}

if p.Lastmod.IsZero() {
if !modified.IsZero() {
p.Lastmod = modified
Expand Down Expand Up @@ -1181,6 +1199,54 @@ func (p *Page) update(f interface{}) error {

}

func (p *Page) updateMetadata() error {

if p.Date.IsZero() && p.s.Cfg.GetBool("useModTimeAsFallback") {

fi, err := p.s.Fs.Source.Stat(filepath.Join(p.s.PathSpec.AbsPathify(p.s.Cfg.GetString("contentDir")), p.File.Path()))
if err == nil {
p.s.Log.DEBUG.Printf("using file modification time as fallback for page %s", p.File.Path())
p.Date = fi.ModTime()
p.Params["date"] = p.Date
}
}

if p.Date.IsZero() && p.s.Cfg.GetBool("useFilenameDateAsFallback") {
match := dateExp.FindStringSubmatch(p.File.Path())

if match != nil {
year := match[1]
month := match[2]
day := match[3]
slug := match[4]

filenameDate, err := time.Parse("2006-01-02", year+"-"+month+"-"+day)

if err == nil {
p.Date = filenameDate
p.Params["date"] = p.Date
if p.Slug == "" {
p.Slug = slug
p.s.Log.DEBUG.Printf("Using filename date (%s) and slug (%s) as fallback for page %s", p.Date, p.Slug, p.File.Path())

} else {
p.s.Log.DEBUG.Printf("Using filename date (%s) as fallback for page %s", p.Date, p.File.Path())

}
} else {
p.s.Log.WARN.Printf("File has what looks like a date, but the date is invalid for page %s.", p.File.Path())
}
}
}

if p.Lastmod.IsZero() {
p.Lastmod = p.Date
}
p.Params["lastmod"] = p.Lastmod
return nil

}

func (p *Page) GetParam(key string) interface{} {
return p.getParam(key, true)
}
Expand Down Expand Up @@ -1419,10 +1485,11 @@ func (p *Page) parse(reader io.Reader) error {
}

if meta != nil {
if err = p.update(meta); err != nil {
if err = p.readMetadata(meta); err != nil {
return err
}
}
p.updateMetadata()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is added so when you render pages that are missing frontmatter they don't get more broken than they need to be. Took me ages to figure out that title/slug and lastmod changes were not handled unless I had an explicit title in frontmatter. With this change it at least still sorts and gets rendered somewhat properly.


return nil
}
Expand Down
76 changes: 76 additions & 0 deletions hugolib/page_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Content of the file goes Here
`

simplePageRFC3339Date = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content"
simplePageNoDate = "---\ntitle: Path Date\n---\n Date param from url"
simplePageJSONMultiple = `
{
"title": "foobar",
Expand Down Expand Up @@ -515,6 +516,7 @@ func checkPageDate(t *testing.T, page *Page, time time.Time) {
if page.Date != time {
t.Fatalf("Page date is: %s. Expected: %s", page.Date, time)
}

}

func checkTruncation(t *testing.T, page *Page, shouldBe bool, msg string) {
Expand Down Expand Up @@ -854,6 +856,80 @@ func TestPageWithDate(t *testing.T) {
d, _ := time.Parse(time.RFC3339, "2013-05-17T16:59:30Z")

checkPageDate(t, p, d)

}

func TestDateAsFallbackWithDateInFilenameDisabled(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()

cfg.Set("useFilenameDateAsFallback", false)

writeSource(t, fs, filepath.Join("content", "2017-01-31-simple.md"), simplePageNoDate)

s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})

require.Len(t, s.RegularPages, 1)

p := s.RegularPages[0]

assert.True(t, p.Date.IsZero(), "page date should be empty as no date in file nor filename.")

assert.Equal(t, "", p.Slug, "slug for page should not be set as filename as date is disabled.")
}

func TestDateAsFallbackWithDateInFilename(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()

writeSource(t, fs, filepath.Join("content", "2017-01-31-simple.md"), simplePageNoDate)

s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})

require.Len(t, s.RegularPages, 1)

p := s.RegularPages[0]
d, err := time.Parse("2006-01-02", "2017-01-31")

assert.Equal(t, err, nil)

checkPageDate(t, p, d)

assert.Equal(t, "simple", p.Slug, "slug for page should be set as there is both date and title in filename")
}

func TestDateAsFallbackWithOutDateInFilename(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()

writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageNoDate)

s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})

require.Len(t, s.RegularPages, 1)

p := s.RegularPages[0]

assert.True(t, p.Date.IsZero(), "page date should be empty as no date in filename")
assert.Equal(t, "", p.Slug, "page slug should not be set as no date in filename")

}

func TestDateAsFallbackWithInvalidDateInFilename(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()

writeSource(t, fs, filepath.Join("content", "2017-31-31-simple.md"), simplePageNoDate)

s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true})

require.Len(t, s.RegularPages, 1)

p := s.RegularPages[0]

assert.True(t, p.Date.IsZero(), "page date should be empty as no valid date in filename")
assert.Equal(t, "", p.Slug, "page slug should not be set as no valid date in filename")

}

func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) {
Expand Down