all repos — homestead @ main

Code for my website

templates/layout.go (view raw)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package templates

import (
	"io"

	"github.com/alanpearce/htmlformat"
	"gitlab.com/tozd/go/errors"
	"go.alanpearce.eu/homestead/internal/config"

	g "go.alanpearce.eu/gomponents"
	. "go.alanpearce.eu/gomponents/html"
)

type SiteSettings struct {
	Title            string
	Language         string
	Timezone         config.Timezone
	Menu             []config.MenuItem
	InjectLiveReload bool
}

type Attrs map[string]string

type PageSettings struct {
	Title      string
	TitleAttrs Attrs
	BodyAttrs  Attrs
}

func ExtendAttrs(base Attrs, attrs Attrs) g.Node {
	m := base
	for key, value := range attrs {
		if v, found := base[key]; found {
			m[key] = v + " " + value
		} else {
			m[key] = value
		}
	}

	return g.MapMap(m, func(k string, v string) g.Node {
		return g.Attr(k, v) // can't inline this because it uses ...value, grr
	})
}

func Layout(site SiteSettings, page PageSettings, children ...g.Node) g.Node {
	return DoctypeHTML(FormattedDocument(
		HTML(
			Lang(site.Language),
			Head(
				Meta(Name("viewport"), Content("width=device-width, initial-scale=1.0")),
				TitleEl(g.Text(page.Title)),
				Link(
					Rel("stylesheet"),
					Type("text/css"),
					Href("/style.css"),
				),
				Link(
					Rel("alternate"),
					Type("application/atom+xml"),
					Title(site.Title),
					Href("/atom.xml"),
				),
			),
			Body(
				ExtendAttrs(page.BodyAttrs, nil),
				A(Class("skip"), Href("#main"), g.Text("Skip to main content")),
				Header(
					H2(
						A(
							ExtendAttrs(Attrs{
								"class": "title p-name",
								"href":  "/",
							}, page.TitleAttrs),
							g.Text(site.Title)),
					),
					Nav(
						g.Map(site.Menu, MenuLink),
					),
				),
				Main(ID("main"), g.Group(children)),
				Footer(
					A(Href("https://git.alanpearce.eu/website"), g.Text("Content")),
					g.Text(" is "),
					A(
						Rel("license"),
						Href("http://creativecommons.org/licenses/by/4.0/"),
						g.Text("CC BY 4.0"),
					),
					g.Text(". "),
					A(Href("https://git.alanpearce.eu/homestead"), g.Text("Site source code")),
					g.Text(" is "),
					A(Href("https://opensource.org/licenses/MIT"), g.Text("MIT")),
				),
				g.If(site.InjectLiveReload,
					LiveReload,
				),
			),
		)))
}

var LiveReload = Script(Defer(), g.Raw(`
	new EventSource("/_/reload").onmessage = event => {
		console.log("got message", event)
		window.location.reload()
	};
`))

func MenuLink(item config.MenuItem) g.Node {
	return A(
		Href(item.URL.String()),
		g.If(item.URL.IsAbs(), Target("_blank")),
		g.Text(item.Name),
	)
}

func FormattedDocument(doc g.Node) g.Node {
	return g.NodeFunc(func(w io.Writer) error {
		pr, pw := io.Pipe()
		defer pr.Close()

		go func() {
			pw.CloseWithError(doc.Render(pw))
		}()

		if err := htmlformat.Document(w, pr); err != nil {
			return errors.WithStack(err)
		}

		return nil
	})
}

func DoctypeHTML(sibling g.Node) g.Node {
	return g.NodeFunc(func(w io.Writer) error {
		if _, err := w.Write([]byte("<!doctype html>\n")); err != nil {
			return err
		}

		return sibling.Render(w)
	})
}