about summary refs log tree commit diff stats
path: root/templates
diff options
context:
space:
mode:
Diffstat (limited to 'templates')
-rw-r--r--templates/404.html37
-rw-r--r--templates/atom.xml48
-rw-r--r--templates/dev.go9
-rw-r--r--templates/embed.go8
-rw-r--r--templates/error.templ17
-rw-r--r--templates/feed-styles.xsl6
-rw-r--r--templates/feed.xml24
-rw-r--r--templates/homepage.html65
-rw-r--r--templates/homepage.templ42
-rw-r--r--templates/list.html55
-rw-r--r--templates/list.templ51
-rw-r--r--templates/page.templ116
-rw-r--r--templates/post.html75
-rw-r--r--templates/post.templ59
-rw-r--r--templates/robots.tmpl6
-rw-r--r--templates/style.css234
-rw-r--r--templates/tags.html41
-rw-r--r--templates/tags.templ23
18 files changed, 460 insertions, 456 deletions
diff --git a/templates/404.html b/templates/404.html
deleted file mode 100644
index 4e64fcc..0000000
--- a/templates/404.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Site Title</title>
-    <meta name="referrer" content="no-referrer-when-downgrade" />
-    <link
-      rel="alternate"
-      type="application/atom+xml"
-      title=""
-      href="/atom.xml"
-    />
-    <style></style>
-  </head>
-  <body>
-    <a class="skip" href="#main">Skip to main content</a>
-    <header>
-      <h2>
-        <a href="/" class="title">Site title</a>
-      </h2>
-      <nav>
-        <a href="/">Home</a>
-      </nav>
-    </header>
-    <main id="main">
-      <h1>404</h1>
-      <h2>ʕノ•ᴥ•ʔノ ︵ ┻━┻</h2>
-    </main>
-    <footer>
-      Licensed under a
-      <a rel="license" href="http://creativecommons.org/licenses/by/4.0/"
-        >Creative Commons Attribution 4.0 International License</a
-      >.
-    </footer>
-  </body>
-</html>
diff --git a/templates/atom.xml b/templates/atom.xml
deleted file mode 100644
index 81c9a76..0000000
--- a/templates/atom.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<?xml-stylesheet href="/feed-styles.xsl" type="text/xsl"?>
-<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}">
-    <title>{{ config.title }}
-    {%- if term %} - {{ term.name }}
-    {%- elif section.title %} - {{ section.title }}
-    {%- endif -%}
-    </title>
-    {%- if config.description %}
-    <subtitle>{{ config.description }}</subtitle>
-    {%- endif %}
-    <link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/>
-    <link href="
-      {%- if section -%}
-        {{ section.permalink | escape_xml | safe }}
-      {%- else -%}
-        {{ config.base_url | escape_xml | safe }}
-      {%- endif -%}
-    "/>
-    <generator uri="https://www.getzola.org/">Zola</generator>
-    <updated>{{ last_updated | date(format="%+") }}</updated>
-    <id>{{ feed_url | safe }}</id>
-    {%- for page in pages %}
-    <entry xml:lang="{{ page.lang }}">
-        <title>{{ page.title }}</title>
-        <published>{{ page.date | date(format="%+") }}</published>
-        <updated>{{ page.updated | default(value=page.date) | date(format="%+") }}</updated>
-        <author>
-          <name>
-            {%- if page.authors -%}
-              {{ page.authors[0] }}
-            {%- elif config.author -%}
-              {{ config.author }}
-            {%- else -%}
-              Unknown
-            {%- endif -%}
-          </name>
-        </author>
-        <link rel="alternate" href="{{ page.permalink | safe }}" type="text/html"/>
-        <id>{{ page.permalink | safe }}</id>
-        {% if page.summary %}
-        <summary type="html">{{ page.summary }}</summary>
-        {% else %}
-        <content type="html">{{ page.content }}</content>
-        {% endif %}
-    </entry>
-    {%- endfor %}
-</feed>
diff --git a/templates/dev.go b/templates/dev.go
new file mode 100644
index 0000000..37a6416
--- /dev/null
+++ b/templates/dev.go
@@ -0,0 +1,9 @@
+//go:build !embed
+
+package templates
+
+import (
+	"os"
+)
+
+var Files = os.DirFS("templates/")
diff --git a/templates/embed.go b/templates/embed.go
new file mode 100644
index 0000000..e7e1f18
--- /dev/null
+++ b/templates/embed.go
@@ -0,0 +1,8 @@
+//go:build embed
+
+package templates
+
+import "embed"
+
+//go:embed *
+var Files embed.FS
diff --git a/templates/error.templ b/templates/error.templ
new file mode 100644
index 0000000..369cb83
--- /dev/null
+++ b/templates/error.templ
@@ -0,0 +1,17 @@
+package templates
+
+import (
+	"go.alanpearce.eu/website/internal/config"
+	"go.alanpearce.eu/website/internal/http"
+	"strconv"
+)
+
+templ Error(config *config.Config, path string, err *http.Error) {
+	@Page(config, PageSettings{
+		Title: "Error",
+		Path:  path,
+	}) {
+		<h1>{ strconv.Itoa(err.Code) } { err.Message }</h1>
+		<h2>ʕノ•ᴥ•ʔノ ︵ ┻━┻</h2>
+	}
+}
diff --git a/templates/feed-styles.xsl b/templates/feed-styles.xsl
index 5953f89..679d064 100644
--- a/templates/feed-styles.xsl
+++ b/templates/feed-styles.xsl
@@ -12,7 +12,9 @@
         <meta charset="utf-8" />
         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1" />
-        <style></style>
+        <style>
+          {{ .css }}
+        </style>
       </head>
       <body>
         <main>
@@ -28,7 +30,7 @@
               <svg
                 xmlns="http://www.w3.org/2000/svg"
                 version="1.1"
-                style="width: 1.5ex; height: 1.5ex"
+                class="rss-icon"
                 viewBox="0 0 256 256"
               >
                 <rect width="256" height="256" x="0" y="0" fill="#7F7F7F" />
diff --git a/templates/feed.xml b/templates/feed.xml
deleted file mode 100644
index ddc90dd..0000000
--- a/templates/feed.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<?xml-stylesheet href="/feed-styles.xsl" type="text/xsl"?>
-<feed xmlns="http://www.w3.org/2005/Atom">
-  <title>Example Feed</title>
-  <link href="http://example.org/"></link>
-  <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
-  <updated>2003-12-13T18:30:02Z</updated>
-  <entry>
-    <title>Atom-Powered Robots Run Amok</title>
-    <link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"></link>
-    <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
-    <updated>2003-12-13T18:30:02Z</updated>
-    <summary>Some text.</summary>
-    <content type="html">
-      <div>
-        <p>This is the entry content.</p>
-      </div>
-    </content>
-    <author>
-      <name>John Doe</name> 
-    </author>
-  </entry>
-
-</feed>
diff --git a/templates/homepage.html b/templates/homepage.html
deleted file mode 100644
index 5004340..0000000
--- a/templates/homepage.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!doctype html>
-<html>
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Site Title</title>
-    <meta name="referrer" content="no-referrer-when-downgrade" />
-    <link
-      rel="alternate"
-      type="application/atom+xml"
-      title=""
-      href="/atom.xml"
-    />
-    <style></style>
-  </head>
-  <body>
-    <a class="skip" href="#main">Skip to main content</a>
-    <header>
-      <h2>
-        <a href="/" class="title">Site title</a>
-      </h2>
-      <nav>
-        <a href="/">Home</a>
-      </nav>
-    </header>
-    <main id="main">
-      <div id="content"></div>
-      <section>
-        <h2>Latest Posts</h2>
-        <ul class="h-feed">
-          <li class="h-entry">
-            <span>
-              <time
-                class="dt-published"
-                datetime="2000-12-31T12:33:02+02:00"
-                pubdate
-              >
-                2000-12-31
-              </time>
-            </span>
-            <a class="p-name u-url" href="/post/lorem-ipsum/">Lorem Ipsum</a>
-          </li>
-        </ul>
-      </section>
-      <section>
-        <h2>Elsewhere on the Internet</h2>
-        <ul class="elsewhere">
-          <li>
-            <a class="u-email" rel="me" href="mailto:user@example.com"
-            >user@example.com</a></li>
-          <li>
-            <a class="u-url" rel="me" href="http://example.com">Example</a>
-          </li>
-          </li>
-        </ul>
-      </section>
-    </main>
-    <footer>
-      Licensed under a
-      <a rel="license" href="http://creativecommons.org/licenses/by/4.0/"
-        >Creative Commons Attribution 4.0 International License</a
-      >.
-    </footer>
-  </body>
-</html>
diff --git a/templates/homepage.templ b/templates/homepage.templ
new file mode 100644
index 0000000..aa61c40
--- /dev/null
+++ b/templates/homepage.templ
@@ -0,0 +1,42 @@
+package templates
+
+import (
+	"go.alanpearce.eu/website/internal/config"
+	"go.alanpearce.eu/website/internal/content"
+)
+
+templ Homepage(config *config.Config, posts []content.Post, content string) {
+	@Page(config, PageSettings{
+		Title: config.Title,
+		TitleAttrs: templ.Attributes{
+			"class": "p-name u-url",
+		},
+		Path: "/",
+		BodyAttrs: templ.Attributes{
+			"class": "h-card",
+		},
+	}) {
+		<div id="content">
+			@Unsafe(content)
+		</div>
+		<section>
+			<h2>Latest Posts</h2>
+			@list(posts[0:3])
+		</section>
+		<section>
+			<h2>Elsewhere on the Internet</h2>
+			<ul class="elsewhere">
+				<li>
+					<a class="u-email" rel="me" href={ templ.SafeURL("mailto:" + config.Email) }>
+						{ config.Email }
+					</a>
+				</li>
+				for _, link := range config.Menus["me"] {
+					<li>
+						<a class="u-url" rel="me" href={ templ.SafeURL(link.URL.String()) }>{ link.Name }</a>
+					</li>
+				}
+			</ul>
+		</section>
+	}
+}
diff --git a/templates/list.html b/templates/list.html
deleted file mode 100644
index 77415ba..0000000
--- a/templates/list.html
+++ /dev/null
@@ -1,55 +0,0 @@
-<!doctype html>
-<html lang="en-GB">
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Site Title</title>
-    <meta name="referrer" content="no-referrer-when-downgrade" />
-    <link
-      rel="alternate"
-      type="application/atom+xml"
-      title="Site Title"
-      href="/atom.xml"
-    />
-    <style></style>
-  </head>
-  <body>
-    <a class="skip" href="#content">Skip to main content</a>
-    <header>
-      <h2>
-        <a href="/" class="title">Site Title</a>
-      </h2>
-      <nav>
-        <a href="/">Home</a>
-      </nav>
-    </header>
-    <main id="content">
-      <div class="filter">
-        <h3 class="filter">Tag</h3>
-        <small>
-          <a href="../">Remove filter</a>
-        </small>
-      </div>
-      <ul class="h-feed">
-        <li class="h-entry">
-          <span>
-            <time
-              class="dt-published"
-              datetime="2000-12-31T12:33:02+02:00"
-              pubdate
-            >
-              2000-12-31
-            </time>
-          </span>
-          <a class="p-name u-url" href="/post/lorem-ipsum/">Lorem Ipsum</a>
-        </li>
-      </ul>
-    </main>
-    <footer>
-      Licensed under a
-      <a rel="license" href="http://creativecommons.org/licenses/by/4.0/"
-        >Creative Commons Attribution 4.0 International License</a
-      >.
-    </footer>
-  </body>
-</html>
diff --git a/templates/list.templ b/templates/list.templ
new file mode 100644
index 0000000..fc59677
--- /dev/null
+++ b/templates/list.templ
@@ -0,0 +1,51 @@
+package templates
+
+import (
+	"go.alanpearce.eu/website/internal/config"
+	"go.alanpearce.eu/website/internal/content"
+)
+
+templ TagPage(config *config.Config, tag string, posts []content.Post, path string) {
+	@Page(config, PageSettings{
+		Title: tag,
+		Path:  path,
+		TitleAttrs: templ.Attributes{
+			"class": "p-author h-card",
+			"rel":   "author",
+		},
+	}) {
+		<div class="filter">
+			<h3 class="filter">#{ tag }</h3>
+			<small>
+				<a href="../">Remove filter</a>
+			</small>
+		</div>
+		@list(posts)
+	}
+}
+
+templ ListPage(config *config.Config, posts []content.Post, path string) {
+	@Page(config, PageSettings{
+		Title: config.Title,
+		TitleAttrs: templ.Attributes{
+			"class": "p-author h-card",
+			"rel":   "author",
+		},
+		Path: path,
+	}) {
+		@list(posts)
+	}
+}
+
+templ list(posts []content.Post) {
+	<ul class="h-feed">
+		for _, post := range posts {
+			<li class="h-entry">
+				<span>
+					@postDate(post.Date)
+				</span>
+				<a class="p-name u-url" href={ templ.SafeURL(post.URL) }>{ post.Title }</a>
+			</li>
+		}
+	</ul>
+}
diff --git a/templates/page.templ b/templates/page.templ
new file mode 100644
index 0000000..294b73e
--- /dev/null
+++ b/templates/page.templ
@@ -0,0 +1,116 @@
+package templates
+
+import (
+	"context"
+	"go.alanpearce.eu/website/internal/config"
+	"io"
+	"io/fs"
+	"net/url"
+)
+
+var (
+	css string
+)
+
+func Setup() {
+	bytes, err := fs.ReadFile(Files, "style.css")
+	if err != nil {
+		panic(err)
+	}
+	css = string(bytes)
+}
+
+type PageSettings struct {
+	Title      string
+	Path       string
+	TitleAttrs templ.Attributes
+	BodyAttrs  templ.Attributes
+}
+
+func extendClasses(cs string, attrs templ.Attributes) string {
+	if extras, exists := attrs["class"]; exists {
+		return templ.Classes(cs, extras).String()
+	} else {
+		return cs
+	}
+}
+
+templ menuItem(item config.MenuItem) {
+	<a
+		href={ templ.SafeURL(item.URL.String()) }
+		if item.URL.IsAbs() {
+			target="_blank"
+		}
+	>{ item.Name }</a>
+}
+
+templ Page(site *config.Config, page PageSettings) {
+	<!DOCTYPE html>
+	<html lang={ site.DefaultLanguage }>
+		<head>
+			<meta charset="utf-8"/>
+			<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+			<title>{ page.Title }</title>
+			<link rel="alternate" type="application/atom+xml" title={ site.Title } href="/atom.xml"/>
+			@style(css)
+		</head>
+		<body { page.BodyAttrs... }>
+			<a class="skip" href="#main">Skip to main content</a>
+			<header>
+				<h2>
+					<a href="/" class={ extendClasses("title p-name", page.TitleAttrs) } { page.TitleAttrs... }>{ site.Title }</a>
+				</h2>
+				<nav>
+					for _, item := range site.Menus["main"] {
+						@menuItem(item)
+					}
+				</nav>
+			</header>
+			<main id="main">
+				{ children... }
+			</main>
+			<footer>
+				Content is
+				<a rel="license" href="http://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>.
+				<a href="https://git.alanpearce.eu/website/">Site source code</a> is
+				<a href="https://opensource.org/licenses/MIT">MIT</a>
+			</footer>
+			@counter(site, page.Path, page.Title)
+			if site.InjectLiveReload {
+				<script defer>
+					new EventSource("/_/reload").onmessage = event => {
+						console.log("got message", event)
+						window.location.reload()
+					};
+				</script>
+			}
+		</body>
+	</html>
+}
+
+func mkURL(original config.URL, path string, title string) string {
+	ou := *original.URL
+	u := config.URL{
+		URL: &ou,
+	}
+	q := url.Values{}
+	q.Add("p", path)
+	q.Add("t", title)
+	u.RawQuery = q.Encode()
+
+	return u.String()
+}
+
+templ counter(config *config.Config, path string, title string) {
+	<script data-goatcounter={ config.GoatCounter.String() } async src="https://stats.alanpearce.eu/count.v4.js" crossorigin="anonymous" integrity="sha384-nRw6qfbWyJha9LhsOtSb2YJDyZdKvvCFh0fJYlkquSFjUxp9FVNugbfy8q1jdxI+"></script>
+	<noscript>
+		<img src={ string(templ.URL(mkURL(config.GoatCounter, path, title))) }/>
+	</noscript>
+}
+
+func style(css string) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
+		_, err = io.WriteString(w, "<style>\n"+css+"\n</style>")
+		return
+	})
+}
diff --git a/templates/post.html b/templates/post.html
deleted file mode 100644
index 1262ee0..0000000
--- a/templates/post.html
+++ /dev/null
@@ -1,75 +0,0 @@
-<!doctype html>
-<html lang="en-GB">
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title></title>
-    <meta name="referrer" content="no-referrer-when-downgrade" />
-    <link
-      rel="alternate"
-      type="application/atom+xml"
-      title=""
-      href="/atom.xml"
-    />
-    <style></style>
-  </head>
-  <body>
-    <a class="skip" href="#main">Skip to main content</a>
-    <header>
-      <h2>
-        <a href="/" class="title"></a>
-      </h2>
-      <nav>
-        <a href="/">Home</a>
-      </nav>
-    </header>
-    <main id="main">
-      <article class="h-entry">
-        <h1 class="p-name">Post Title</h1>
-        <p>
-          <time class="dt-published" pubdate>2000-12-31</time>
-        </p>
-        <div class="e-content">
-          Enim lobortis scelerisque fermentum dui faucibus in ornare quam
-          viverra. Eget egestas purus viverra accumsan in nisl nisi, scelerisque
-          eu ultrices vitae, auctor eu augue ut lectus arcu, bibendum at.
-
-          <code>/bin/test</code>
-
-          <pre>
-            <code class="language-conf">
-foo=bar
-            </code>
-          </pre>
-
-          <table>
-            <thead>
-              <tr>
-                <th>One</th>
-                <th>Two</th>
-                <th>Three</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr>
-                <td>1</td>
-                <td>2</td>
-                <td>3</td>
-              </tr>
-            </tbody>
-          </table>
-        </div>
-        <ul class="p-categories tags">
-          Tags:
-          <li><a class="p-category" href="/tags/sample/">#sample</a></li>
-        </ul>
-      </article>
-    </main>
-    <footer>
-      Licensed under a
-      <a rel="license" href="http://creativecommons.org/licenses/by/4.0/"
-        >Creative Commons Attribution 4.0 International License</a
-      >.
-    </footer>
-  </body>
-</html>
diff --git a/templates/post.templ b/templates/post.templ
new file mode 100644
index 0000000..1a5495c
--- /dev/null
+++ b/templates/post.templ
@@ -0,0 +1,59 @@
+package templates
+
+import (
+	"context"
+	"io"
+	"time"
+
+	"go.alanpearce.eu/website/internal/config"
+	"go.alanpearce.eu/website/internal/content"
+)
+
+func Unsafe(html string) templ.Component {
+	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
+		_, err = io.WriteString(w, html)
+		return
+	})
+}
+
+templ postDate(d time.Time) {
+	<time class="dt-published" datetime={ d.UTC().Format(time.RFC3339) }>
+		{ d.Format("2006-01-02") }
+	</time>
+}
+
+templ PostPage(config *config.Config, post content.Post) {
+	@Page(config, PageSettings{
+		Title: post.Title,
+		TitleAttrs: templ.Attributes{
+			"class": "p-author h-card",
+			"rel":   "author",
+		},
+		BodyAttrs: templ.Attributes{
+			"class": "h-entry",
+		},
+		Path: post.URL,
+	}) {
+		<article>
+			<h1 class="p-name">{ post.Title }</h1>
+			<p>
+				<a class="u-url" href={ templ.SafeURL(post.URL) }>
+					@postDate(post.Date)
+				</a>
+			</p>
+			<div class="e-content">
+				@Unsafe(post.Content)
+			</div>
+			<div class="tags">
+				Tags:
+				<ul class="p-categories tags">
+					for _, tag := range post.Taxonomies.Tags {
+						<li>
+							@tagLink(tag, templ.Attributes{"class": "p-category"})
+						</li>
+					}
+				</ul>
+			</div>
+		</article>
+	}
+}
diff --git a/templates/robots.tmpl b/templates/robots.tmpl
new file mode 100644
index 0000000..8ccad4d
--- /dev/null
+++ b/templates/robots.tmpl
@@ -0,0 +1,6 @@
+User-agent: *
+Disallow:
+{{- with .BaseURL }}
+Host: {{ .Hostname }}
+Sitemap: {{ .JoinPath "/sitemap.xml" }}
+{{- end }}
diff --git a/templates/style.css b/templates/style.css
index 5c55ef4..e9a2955 100644
--- a/templates/style.css
+++ b/templates/style.css
@@ -1,23 +1,43 @@
+:root {
+  --width: 800px;
+  --font-main: Verdana, sans-serif;
+  --font-secondary: Verdana, sans-serif;
+  --font-scale: 1em;
+  --background-color: #fff;
+  --heading-color: #222;
+  --text-color: #444;
+  --link-color: #3273dc;
+  --visited-color: #8b6fcb;
+  --code-background-color: #f2f2f2;
+  --code-color: #222;
+  --blockquote-color: #222;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --background-color: #01242e;
+    --heading-color: #eee;
+    --text-color: #ddd;
+    --link-color: #8cc2dd;
+    --visited-color: #8b6fcb;
+    --code-background-color: #000;
+    --code-color: #ddd;
+    --blockquote-color: #ccc;
+  }
+}
+
 body {
-  font-family: Verdana, sans-serif;
+  font-family: var(--font-secondary);
+  font-size: var(--font-scale);
   margin: auto;
-  padding: 1em;
-  max-width: 50rem;
+  padding: 20px;
+  max-width: var(--width);
   text-align: left;
-  background-color: #fff;
+  background-color: var(--background-color);
   word-wrap: break-word;
   overflow-wrap: break-word;
   line-height: 1.5;
-  color: #444;
-}
-
-.skip {
-  position: absolute;
-  top: -3em;
-  background: #fff;
-}
-.skip:focus {
-  top: 0;
+  color: var(--text-color);
 }
 
 h1,
@@ -25,56 +45,34 @@ h2,
 h3,
 h4,
 h5,
-h6,
-strong,
-b {
-  color: #222;
+h6 {
+  font-family: var(--font-main);
+  color: var(--heading-color);
+  & > a {
+    color: var(--heading-color);
+  }
 }
 
 a {
-  color: #3273dc;
-}
-
-.title {
-  color: #222;
-  text-decoration: none;
-  border: 0;
-}
-
-.filter {
-  margin-bottom: 0;
-}
-
-time {
-  font-style: italic;
+  color: var(--link-color);
+  cursor: pointer;
 }
 
 nav a {
-  margin-right: 1ex;
+  margin-right: 8px;
 }
 
-.tags {
-  padding: unset;
-  font-size: small;
-}
-
-.tags > li {
-  list-style: none;
-  display: inline-block;
-  padding-right: 1ex;
-}
-
-textarea {
-  width: 100%;
-  font-size: 1rem;
+strong,
+b {
+  color: var(--heading-color);
 }
 
-input {
-  font-size: 1rem;
+button {
+  margin: 0;
+  cursor: pointer;
 }
 
-main,
-article {
+main {
   line-height: 1.6;
 }
 
@@ -82,17 +80,29 @@ table {
   width: 100%;
 }
 
+hr {
+  border: 0;
+  border-top: 1px dashed;
+}
+
 img {
   max-width: 100%;
 }
 
 code {
+  font-family: monospace;
   padding: 2px 5px;
-  background-color: #f2f2f2;
+  background-color: var(--code-background-color);
+  color: var(--code-color);
+  border-radius: 3px;
+}
+
+pre {
+  margin: unset;
 }
 
 pre code {
-  color: #222;
+  color: var(--code-color);
   display: block;
   padding: 20px;
   white-space: pre-wrap;
@@ -100,34 +110,47 @@ pre code {
   overflow-x: auto;
 }
 
-div.highlight pre {
-  background-color: initial;
-  color: initial;
-}
-
-div.highlight code {
-  background-color: unset;
-  color: unset;
-}
-
-blockquote {
+blockquote,
+.aside {
   border-left: 1px solid #999;
-  color: #222;
+  color: var(--code-color);
   padding-left: 20px;
   font-style: italic;
 }
 
+.aside {
+  font-style: unset;
+}
+
 footer {
-  padding: 25px;
+  padding: 25px 0;
   text-align: center;
 }
 
-.helptext {
-  color: #777;
-  font-size: small;
+.title:hover {
+  text-decoration: none;
+}
+
+.title h1 {
+  font-size: 1.5em;
+}
+
+.inline {
+  width: auto !important;
 }
 
-/* blog posts */
+.highlight,
+.code {
+  padding: 1px 15px;
+  background-color: var(--code-background-color);
+  color: var(--code-color);
+  border-radius: 3px;
+  margin-block-start: 1em;
+  margin-block-end: 1em;
+  overflow-x: auto;
+}
+
+/* blog post list */
 ul.h-feed {
   list-style-type: none;
   padding: unset;
@@ -142,50 +165,43 @@ ul.h-feed li span {
 }
 
 ul.h-feed li a:visited {
-  color: #8b6fcb;
+  color: var(--visited-color);
 }
 
-@media (prefers-color-scheme: dark) {
-  body {
-    background-color: #333;
-    color: #ddd;
-  }
-
-  h1,
-  h2,
-  h3,
-  h4,
-  h5,
-  h6,
-  strong,
-  b,
-  .title {
-    color: #eee;
-  }
+.title {
+  text-decoration: none;
+}
 
-  a {
-    color: #8cc2dd;
-  }
+.filter {
+  margin-bottom: 0;
+}
 
-  code {
-    background-color: #777;
-  }
+.skip {
+  position: absolute;
+  top: -3em;
+  background: var(--background-color);
+}
 
-  pre code {
-    color: #ddd;
-  }
+.skip:focus {
+  top: 0;
+}
 
-  blockquote {
-    color: #ccc;
-  }
+.tags {
+  font-size: small;
+  display: inline-block;
+  padding-inline-start: 0;
+}
+.p-categories {
+  padding-inline-start: 1ex;
+}
 
-  textarea,
-  input {
-    background-color: #252525;
-    color: #ddd;
-  }
+.tags li {
+  list-style: none;
+  display: inline-block;
+  margin-inline-end: 1ex;
+}
 
-  .helptext {
-    color: #aaa;
-  }
+svg.rss-icon {
+  height: 1.5ex;
+  width: 1.5ex;
 }
diff --git a/templates/tags.html b/templates/tags.html
deleted file mode 100644
index 406f8d7..0000000
--- a/templates/tags.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!doctype html>
-<html lang="en-GB">
-  <head>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Site Title</title>
-    <meta name="referrer" content="no-referrer-when-downgrade" />
-    <link
-      rel="alternate"
-      type="application/atom+xml"
-      title="Site title"
-      href="/atom.xml"
-    />
-    <style></style>
-  </head>
-  <body>
-    <a class="skip" href="#content">Skip to main content</a>
-    <header>
-      <h2>
-        <a href="/" class="title">Site title</a>
-      </h2>
-      <nav>
-        <a href="/">Home</a>
-      </nav>
-    </header>
-    <main id="content">
-      <h3 class="filter">Tags</h3>
-      <ul class="tags">
-        <li class="h-feed">
-          <a href="/tags/tag">#tag</a>
-        </li>
-      </ul>
-    </main>
-    <footer>
-      Licensed under a
-      <a rel="license" href="http://creativecommons.org/licenses/by/4.0/"
-        >Creative Commons Attribution 4.0 International License</a
-      >.
-    </footer>
-  </body>
-</html>
diff --git a/templates/tags.templ b/templates/tags.templ
new file mode 100644
index 0000000..c872a0d
--- /dev/null
+++ b/templates/tags.templ
@@ -0,0 +1,23 @@
+package templates
+
+import "go.alanpearce.eu/website/internal/config"
+
+templ tagLink(tag string, attrs templ.Attributes) {
+	<a { attrs... } href={ templ.SafeURL("/tags/" + tag) }>#{ tag }</a>
+}
+
+templ TagsPage(config *config.Config, title string, tags []string, path string) {
+	@Page(config, PageSettings{
+		Title: title,
+		Path:  path,
+	}) {
+		<h3 class="filter">Tags</h3>
+		<ul class="tags">
+			for _, tag := range tags {
+				<li class="h-feed">
+					@tagLink(tag, templ.Attributes{})
+				</li>
+			}
+		</ul>
+	}
+}