diff --git a/blog/blog_test.go b/blog/blog_test.go index 93729aa5..837fa7be 100644 --- a/blog/blog_test.go +++ b/blog/blog_test.go @@ -1790,6 +1790,13 @@ func TestHeadMetadata(t *testing.T) { ``, ``) + // Template actions that print nothing must not leave blank lines (#649). + for _, page := range []string{"/", "/posts/2026/08/15/hello", "/dir/hello", "/search?q=x"} { + if h := head(get(page)); regexp.MustCompile(`\n[ \t]*\n`).MatchString(h) { + t.Errorf("%s head has blank lines:\n%s", page, h) + } + } + // Search results are not for indexing. check(t, "/search?q=x", ``) diff --git a/templates/shared/_head.html b/templates/shared/_head.html index 58b568d6..4dced4c1 100644 --- a/templates/shared/_head.html +++ b/templates/shared/_head.html @@ -1,4 +1,4 @@ -{{ define "_head" }} +{{ define "_head" -}} @@ -7,43 +7,55 @@ {{- /* Search result pages and the like ask not to be indexed; otherwise the robots_tag setting. */}} - {{ if .noindex }} + {{- if .noindex }} - {{ else }} + {{- else }} - {{ end }} + {{- end }} {{- /* Description: the page's own (a plugin page's meta_description, a post's first words), else the site_description setting. */}} - {{ $description := "" }} - {{ if .meta_description }}{{ $description = .meta_description }} - {{ else if .post }}{{ $description = .post.PlainTextPreview 155 }} - {{ else }}{{ with index .settings "site_description" }}{{ $description = .Value }}{{ end }}{{ end }} + {{- $description := "" }} + {{- if .meta_description }}{{ $description = .meta_description }} + {{- else if .post }}{{ $description = .post.PlainTextPreview 155 }} + {{- else }}{{ with index .settings "site_description" }}{{ $description = .Value }}{{ end }}{{ end }} {{- /* Title: ": " — a page that sets no title (the admin pages) gets the site title alone; a missing value must never print as %!s(). */}} - {{ $siteTitle := "" }}{{ with .settings }}{{ with .site_title }}{{ $siteTitle = .Value }}{{ end }}{{ end }} - {{ $pageTitle := "" }}{{ if .post }}{{ $pageTitle = .post.Title }}{{ else }}{{ with .title }}{{ $pageTitle = . }}{{ end }}{{ end }} - {{ $title := $siteTitle }}{{ if and $siteTitle $pageTitle }}{{ $title = printf "%s: %s" $siteTitle $pageTitle }}{{ else if $pageTitle }}{{ $title = $pageTitle }}{{ end }} + {{- $siteTitle := "" }}{{ with .settings }}{{ with .site_title }}{{ $siteTitle = .Value }}{{ end }}{{ end }} + {{- $pageTitle := "" }}{{ if .post }}{{ $pageTitle = .post.Title }}{{ else }}{{ with .title }}{{ $pageTitle = . }}{{ end }}{{ end }} + {{- $title := $siteTitle }}{{ if and $siteTitle $pageTitle }}{{ $title = printf "%s: %s" $siteTitle $pageTitle }}{{ else if $pageTitle }}{{ $title = $pageTitle }}{{ end }} {{- /* site_url is set by blog.Render; pages rendered without it (admin) get "" and no absolute URLs. */}} - {{ $site := "" }}{{ with .site_url }}{{ $site = . }}{{ end }} - {{ $image := "" }} - {{ with index .settings "site_image" }}{{ if .Value }}{{ $image = printf "%s%s" $site .Value }}{{ end }}{{ end }} + {{- $site := "" }}{{ with .site_url }}{{ $site = . }}{{ end }} + {{- $image := "" }} + {{- with index .settings "site_image" }}{{ if .Value }}{{ $image = printf "%s%s" $site .Value }}{{ end }}{{ end }} {{ $title }} - {{ if $description }}{{ end }} - {{ if .canonical_url }}{{ end }} + {{- if $description }} + + {{- end }} + {{- if .canonical_url }} + + {{- end }} - {{ if $description }}{{ end }} - {{ if .canonical_url }}{{ end }} + {{- if $description }} + + {{- end }} + {{- if .canonical_url }} + + {{- end }} - {{ if .post }} + {{- if .post }} - {{ $first := "" }}{{ range .post.ImageURLs $site }}{{ if not $first }}{{ $first = . }}{{ end }}{{ end }} - {{ if $first }}{{ else if $image }}{{ end }} + {{- $first := "" }}{{ range .post.ImageURLs $site }}{{ if not $first }}{{ $first = . }}{{ end }}{{ end }} + {{- if $first }} + + {{- else if $image }} + + {{- end }} - {{ range .post.Tags }} + {{- range .post.Tags }} - {{ end }} + {{- end }} - {{ else }} + {{- else }} - {{ if $image }}{{ end }} - {{ if .is_home }} + {{- if $image }} + + {{- end }} + {{- if .is_home }} {{- /* A personal site says whose it is: the site title is the person's name, and sameAs lists the profiles the Social Icons plugin holds (.plugins.socialicons.links), which is how a search engine ties them to this site; the optional person_* settings add role, employer, school and expertise. Off unless site_is_person is set. */}} - {{ with index .settings "site_is_person" }}{{ if eq .Value "true" }} + {{- with index .settings "site_is_person" }}{{ if eq .Value "true" }} - {{ end }}{{ end }} - {{ end }} - {{ end }} - {{ if $site }}{{ end }} + {{- end }}{{ end }} + {{- end }} + {{- end }} + {{- if $site }} + + {{- end }} - {{ if .admin_page }} + {{- if .admin_page }} - {{ end }} + {{- end }} {{- /* Only the login page uses .btn-social; it matched no rule anywhere else. */}} - {{ if .login_page }} + {{- if .login_page }} - {{ end }} + {{- end }} {{- /* Icons are decorative, so Font Awesome does not have to block the first paint: it loads against the print media type, which the browser fetches at a low priority, and switches to all once it arrives. The @@ -133,20 +163,22 @@ {{- /* Was Tachyons from cdnjs: 73 KB for the four rules in base.css (#630). */}} - {{ if .admin_page }} + {{- if .admin_page }} - {{ end }} + {{- end }} - {{ if .admin_page }} + {{- if .admin_page }} - {{ end }} - {{ with index .settings "custom_header_code" }}{{ if .Value }} + {{- end }} + {{- with index .settings "custom_header_code" }}{{ if .Value }} {{ .Value | rawHTML }} - {{ end }}{{ end }} - {{ with .plugin_head_html }}{{ . | rawHTML }}{{ end }} + {{- end }}{{ end }} + {{- with .plugin_head_html }} + {{ . | rawHTML }} + {{- end }} {{ end }}