diff --git a/hugolib/page_output.go b/hugolib/page_output.go index cdc4a53d8..5fdb87057 100644 --- a/hugolib/page_output.go +++ b/hugolib/page_output.go @@ -14,6 +14,7 @@ package hugolib import ( + "fmt" "html/template" "strings" "sync" @@ -116,20 +117,64 @@ type OutputFormats []*OutputFormat // And OutputFormat links to a representation of a resource. type OutputFormat struct { + // Rel constains a value that can be used to construct a rel link. + // This is value is fetched from the output format definition. + // Note that for pages with only one output format, + // this method will always return "canonical". + // TODO(bep) output -- the above may not be correct for CSS etc. Figure out a way around that. + // TODO(bep) output -- re the above, maybe add a "alternate" filter to AlternativeOutputFormats. + // As an example, the AMP output format will, by default, return "amphtml". + // + // See: + // https://www.ampproject.org/docs/guides/deploy/discovery + // + // Most other output formats will have "alternate" as value for this. + Rel string + + // It may be tempting to export this, but let us hold on to that horse for a while. f output.Format p *Page } +// Name returns this OutputFormat's name, i.e. HTML, AMP, JSON etc. +func (o OutputFormat) Name() string { + return o.f.Name +} + // TODO(bep) outputs consider just save this wrapper on Page. // OutputFormats gives the output formats for this Page. func (p *Page) OutputFormats() OutputFormats { var o OutputFormats + isCanonical := len(p.outputFormats) == 1 for _, f := range p.outputFormats { - o = append(o, &OutputFormat{f: f, p: p}) + rel := f.Rel + if isCanonical { + rel = "canonical" + } + o = append(o, &OutputFormat{Rel: rel, f: f, p: p}) } return o } +// OutputFormats gives the alternative output formats for this PageOutput. +func (p *PageOutput) AlternativeOutputFormats() (OutputFormats, error) { + var o OutputFormats + for _, of := range p.OutputFormats() { + if of.f == p.outputFormat { + continue + } + o = append(o, of) + } + return o, nil +} + +// AlternativeOutputFormats is only available on the top level rendering +// entry point, and not inside range loops on the Page collections. +// This method is just here to inform users of that restriction. +func (p *Page) AlternativeOutputFormats() (OutputFormats, error) { + return nil, fmt.Errorf("AlternativeOutputFormats only available from the top level template context for page %q", p.Path()) +} + // Get gets a OutputFormat given its name, i.e. json, html etc. // It returns nil if not found. func (o OutputFormats) Get(name string) *OutputFormat { diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go index 12746e88b..85b3291fe 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -15,12 +15,14 @@ package hugolib import ( "reflect" + "strings" "testing" "github.com/stretchr/testify/require" "fmt" + "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/output" "github.com/spf13/viper" ) @@ -47,9 +49,19 @@ func TestDefaultOutputDefinitions(t *testing.T) { } } -func TestSiteWithJSONHomepage(t *testing.T) { +func TestSiteWithPageOutputs(t *testing.T) { + for _, outputs := range [][]string{{"html", "json"}, {"json"}} { + t.Run(fmt.Sprintf("%v", outputs), func(t *testing.T) { + doTestSiteWithPageOutputs(t, outputs) + }) + } +} + +func doTestSiteWithPageOutputs(t *testing.T, outputs []string) { t.Parallel() + outputsStr := strings.Replace(fmt.Sprintf("%q", outputs), " ", ", ", -1) + siteConfig := ` baseURL = "http://example.com/blog" @@ -65,19 +77,26 @@ category = "categories" pageTemplate := `--- title: "%s" -outputs: ["html", "json"] +outputs: %s --- # Doc ` th, h := newTestSitesFromConfig(t, siteConfig, - "layouts/_default/list.json", "List JSON|{{ .Title }}|{{ .Content }}", + "layouts/_default/list.json", `List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}| +{{- range .AlternativeOutputFormats -}} +Alt Output: {{ .Name -}}| +{{- end -}}| +{{- range .OutputFormats -}} +Output/Rel: {{ .Name -}}/{{ .Rel }}| +{{- end -}} +`, ) require.Len(t, h.Sites, 1) fs := th.Fs - writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "JSON Home")) + writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "JSON Home", outputsStr)) err := h.Build(BuildCfg{}) @@ -88,17 +107,38 @@ outputs: ["html", "json"] require.NotNil(t, home) - require.Len(t, home.outputFormats, 2) + lenOut := len(outputs) + + require.Len(t, home.outputFormats, lenOut) // TODO(bep) output assert template/text + // There is currently always a JSON output to make it simpler ... + altFormats := lenOut - 1 + hasHTML := helpers.InStringArray(outputs, "html") + th.assertFileContent("public/index.json", + "List JSON", + fmt.Sprintf("Alt formats: %d", altFormats), + ) - th.assertFileContent("public/index.json", "List JSON") + if hasHTML { + th.assertFileContent("public/index.json", + "Alt Output: HTML", + "Output/Rel: JSON/alternate|", + "Output/Rel: HTML/canonical|", + ) + } else { + th.assertFileContent("public/index.json", + "Output/Rel: JSON/canonical|", + ) + } of := home.OutputFormats() - require.Len(t, of, 2) + require.Len(t, of, lenOut) require.Nil(t, of.Get("Hugo")) require.NotNil(t, of.Get("json")) json := of.Get("JSON") + _, err = home.AlternativeOutputFormats() + require.Error(t, err) require.NotNil(t, json) require.Equal(t, "/blog/index.json", json.RelPermalink()) require.Equal(t, "http://example.com/blog/index.json", json.Permalink()) diff --git a/output/outputFormat.go b/output/outputFormat.go index 392414cca..3812030d1 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -23,26 +23,26 @@ import ( var ( // An ordered list of built-in output formats // See https://www.ampproject.org/learn/overview/ - // TODO - // - // canonical AMPType = Format{ Name: "AMP", MediaType: media.HTMLType, BaseName: "index", Path: "amp", + Rel: "amphtml", } CSSType = Format{ Name: "CSS", MediaType: media.CSSType, BaseName: "styles", + Rel: "stylesheet", } HTMLType = Format{ Name: "HTML", MediaType: media.HTMLType, BaseName: "index", + Rel: "canonical", } JSONType = Format{ @@ -50,6 +50,7 @@ var ( MediaType: media.JSONType, BaseName: "index", IsPlainText: true, + Rel: "alternate", } RSSType = Format{ @@ -57,6 +58,7 @@ var ( MediaType: media.RSSType, BaseName: "index", NoUgly: true, + Rel: "alternate", } ) @@ -84,6 +86,16 @@ type Format struct { // The base output file name used when not using "ugly URLs", defaults to "index". BaseName string + // The value to use for rel links + // + // See https://www.w3schools.com/tags/att_link_rel.asp + // + // AMP has a special requirement in this department, see: + // https://www.ampproject.org/docs/guides/deploy/discovery + // I.e.: + // + Rel string + // The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL. Protocol string