Commit bf64c14

Eric Bower  ·  2026-09-30 09:00:35 -0400 EDT
parent 4a1e923
style(prose): minimal design ethos

Our previous prose blog design hasn't changed since we initially released prose.

The previous design was minimalistic but it still made some assumptions about 
how a blog should look.  In order to support customization we added support
for users to upload `_styles.css` and front-matter fields like `with_styles`
to provide full customization.

The main thesis for this change was trying to answer the question: what if blogs
came without styles by default and users are required to theme it themselves?
This would make it so we are not in control of the overall theme of the blog and
provides full customization for the end-user.

As we were experimenting with this design we wanted to include a very minimal
css reset so the default design is consistent across browsers.  We are also
implemented a few rules with this css reset for best practices.

This is our new design ethos:
- Center content on the screen
- 70ch max width
- Headers should be anchored to the content they are heading

That's it.  The rest of the reset file is for consistency across browsers.  With
this change we are also disabling the `with_styles` field to encourage people to
preserve this new design ethos.

Finally as part of this change we restructured the html tree to better adhere
to semantic meaning of those html entities.  We were already mostly there but
some of the headers and footers were restructured slightly.
8 files changed,  +233, -196
+1, -4
......@@ -185,7 +185,6 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
185185
186186 ts, err := router.RenderTemplate(cfg, []string{
187187 cfg.StaticPath("html/blog-default.partial.tmpl"),
188- cfg.StaticPath("html/blog-aside.partial.tmpl"),
189188 cfg.StaticPath("html/blog.page.tmpl"),
190189 })
191190
......@@ -893,9 +892,7 @@ func serveFile(file string, contentType string) http.HandlerFunc {
893892
894893 func createStaticRoutes() []router.Route {
895894 return []router.Route{
896- router.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
897- router.NewRoute("GET", "/smol.css", serveFile("smol.css", "text/css")),
898- router.NewRoute("GET", "/smol-v2.css", serveFile("smol-v2.css", "text/css")),
895+ router.NewRoute("GET", "/reset.css", serveFile("reset.css", "text/css")),
899896 router.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
900897 router.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
901898 router.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
+0, -2
......@@ -5,9 +5,7 @@
55 <meta charset='utf-8'>
66 <meta name="viewport" content="width=device-width, initial-scale=1" />
77 <title>{{template "title" .}}</title>
8-
98 <meta name="keywords" content="blog, blogging, write, writing, hackers, developers, terminal" />
10-
119 {{template "meta" .}}
1210 </head>
1311 <body {{template "attrs" .}}>{{template "body" .}}</body>
+0, -56
......@@ -1,56 +0,0 @@
1-{{define "blog-aside"}}
2-<main class="flex">
3- <section class="flex-1 mr">
4- <div>
5- <h1 class="text-2xl font-bold mt-2">{{.Header.Title}}</h1>
6- {{if .Header.Bio}}<span>{{.Header.Bio}}</span>{{end}}
7- </div>
8-
9- <div id="readme">
10- {{if .Readme.HasText}}
11- <section class="md">
12- {{.Readme.Contents}}
13- </section>
14- {{end}}
15- <ul>
16- {{range .Header.Nav}}
17- <li><a href="{{.URL}}" class="text-lg">{{.Text}}</a></li>
18- {{end}}
19- <li><a href="{{.RSSURL}}" class="text-lg">rss</a></li>
20- </ul>
21- <hr />
22- </div>
23-
24- {{if .HasFilter}}
25- <a href="/">clear filters</a>
26- {{end}}
27-
28- <div class="posts group mt-2">
29- {{range .Posts}}
30- <article>
31- <div class="flex items-center">
32- <time datetime="{{.PublishAtISO}}" class="text-sm post-date">{{.PublishAt}}</time>
33- <span class="text-md flex-1 m-0 transform-none"><a href="{{.URL}}">{{.Title}}</a></span>
34- </div>
35- </article>
36- {{end}}
37- </div>
38- </section>
39-
40- <aside>
41- {{if .Readme.HasText}}
42- <section class="md">
43- {{.Readme.Contents}}
44- </section>
45- {{end}}
46- <nav>
47- <ul>
48- {{range .Header.Nav}}
49- <li><a href="{{.URL}}" class="text-md transform-none">{{.Text}}</a></li>
50- {{end}}
51- <li><a href="{{.RSSURL}}" class="text-md transform-none">rss</a></li>
52- </ul>
53- </nav>
54- </aside>
55-</main>
56-{{end}}
+22, -14
......@@ -1,18 +1,23 @@
11 {{define "blog-default"}}
2-<header class="text-center">
3- <h1 class="text-2xl font-bold mt-2">{{.Header.Title}}</h1>
4- {{if .Header.Bio}}<span>{{.Header.Bio}}</span>{{end}}
2+<header>
3+ <h1>{{.Header.Title}}</h1>
54 <nav>
5+ {{if .Header.Bio}}
6+ <span>{{.Header.Bio}}</span>
7+ <br />
8+ {{end}}
69 {{range .Header.Nav}}
7- <a href="{{.URL}}" class="text-lg transform-none">{{.Text}}</a> |
10+ <a href="{{.URL}}">{{.Text}}</a>&nbsp;
811 {{end}}
9- <a href="{{.RSSURL}}" class="text-lg transform-none">rss</a>
12+ <a href="{{.RSSURL}}">rss</a>
1013 </nav>
14+
1115 <hr />
1216 </header>
17+
1318 <main>
1419 {{if .Readme.HasText}}
15- <section>
20+ <section id="readme">
1621 <article class="md">
1722 {{.Readme.Contents}}
1823 </article>
......@@ -20,18 +25,21 @@
2025 </section>
2126 {{end}}
2227
23- <section class="posts group mt-2">
28+ <section id="posts">
2429 {{if .HasFilter}}
25- <a href={{.URL}}>clear filters</a>
30+ <a href="{{.URL}}">clear filters</a>
2631 {{end}}
32+
33+ <ul role="list">
2734 {{range .Posts}}
28- <article>
29- <div class="flex items-center">
30- <time datetime="{{.PublishAtISO}}" class="text-sm post-date">{{.PublishAt}}</time>
31- <span class="text-md flex-1 m-0 transform-none"><a href="{{.URL}}">{{.Title}}</a></span>
32- </div>
33- </article>
35+ <li>
36+ <article class="post">
37+ <time datetime="{{.PublishAtISO}}">{{.PublishAt}}</time>
38+ <a href="{{.URL}}">{{.Title}}</a>
39+ </article>
40+ </li>
3441 {{end}}
42+ </ul>
3543 </section>
3644 </main>
3745 {{end}}
+42, -53
......@@ -3,61 +3,50 @@
33 {{define "title"}}{{.PageTitle}}{{end}}
44
55 {{define "meta"}}
6-{{if .Header.Favicon}}
7-<link rel="icon" href="{{.Header.Favicon}}">
8-{{else}}
9-<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
6+ {{if .Header.Favicon}}
7+ <link rel="icon" href="{{.Header.Favicon}}">
8+ {{else}}
9+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10+ {{end}}
11+
12+ <meta name="description" content="{{if .Header.Bio}}{{.Header.Bio}}{{else}}{{.Header.Title}}{{end}}" />
13+ <meta property="og:type" content="website">
14+ <meta property="og:site_name" content="{{.Site.Domain}}">
15+ <meta property="og:url" content="{{.URL}}">
16+ <meta property="og:title" content="{{.Header.Title}}">
17+ {{if .Header.Bio}}<meta property="og:description" content="{{.Header.Bio}}">{{end}}
18+
19+ {{if .Header.Image}}
20+ <meta itemprop="image" content="{{.Header.Image}}" />
21+ <meta property="og:image" content="{{.Header.Image}}" />
22+ <meta name="twitter:image" content="{{.Header.Image}}" />
23+ {{else}}
24+ <meta property="og:image:width" content="300" />
25+ <meta property="og:image:height" content="300" />
26+ <meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
27+ <meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
28+ <meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
29+ {{end}}
30+
31+ {{if .Header.ImageCard}}
32+ <meta property="twitter:card" content="{{.Header.ImageCard}}">
33+ {{else}}
34+ <meta property="twitter:card" content="summary">
35+ {{end}}
36+ <meta property="twitter:url" content="{{.URL}}">
37+ <meta property="twitter:title" content="{{.Header.Title}}">
38+ {{if .Header.Bio}}<meta property="twitter:description" content="{{.Header.Bio}}">{{end}}
39+
40+ <link rel="alternate" href="{{.RSSURL}}" type="application/rss+xml" title="RSS feed for {{.Header.Title}}" />
41+
42+ <link rel="stylesheet" href="/reset.css" />
43+ <link rel="stylesheet" href="/syntax.css" />
44+ {{if .HasCSS}}<link rel="stylesheet" href="{{.CssURL}}" />{{end}}
1045 {{end}}
1146
12-<meta name="description" content="{{if .Header.Bio}}{{.Header.Bio}}{{else}}{{.Header.Title}}{{end}}" />
13-
14-<meta property="og:type" content="website">
15-<meta property="og:site_name" content="{{.Site.Domain}}">
16-<meta property="og:url" content="{{.URL}}">
17-<meta property="og:title" content="{{.Header.Title}}">
18-{{if .Header.Bio}}<meta property="og:description" content="{{.Header.Bio}}">{{end}}
19-
20-{{if .Header.Image}}
21-<meta itemprop="image" content="{{.Header.Image}}" />
22-<meta property="og:image" content="{{.Header.Image}}" />
23-
24-<meta name="twitter:image" content="{{.Header.Image}}" />
25-{{else}}
26-<meta property="og:image:width" content="300" />
27-<meta property="og:image:height" content="300" />
28-<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
29-<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
30-
31-<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
32-{{end}}
33-
34-{{if .Header.ImageCard}}
35-<meta property="twitter:card" content="{{.Header.ImageCard}}">
36-{{else}}
37-<meta property="twitter:card" content="summary">
38-{{end}}
39-<meta property="twitter:url" content="{{.URL}}">
40-<meta property="twitter:title" content="{{.Header.Title}}">
41-{{if .Header.Bio}}<meta property="twitter:description" content="{{.Header.Bio}}">{{end}}
42-
43-<link rel="alternate" href="{{.RSSURL}}" type="application/rss+xml" title="RSS feed for {{.Header.Title}}" />
44-{{if .WithStyles}}
45- <link rel="stylesheet" href="/smol.css" />
46-{{else}}
47-{{end}}
48-<link rel="stylesheet" href="/syntax.css" />
49-{{if .HasCSS}}<link rel="stylesheet" href="{{.CssURL}}" />{{end}}
50-{{end}}
51-
52-{{define "attrs"}}id="blog" class="layout-{{.Header.Layout}}"{{end}}
47+{{define "attrs"}}id="blog"{{end}}
5348
5449 {{define "body"}}
55-
56-{{if eq .Header.Layout "aside"}}
57- {{template "blog-aside" .}}
58-{{else}}
59- {{template "blog-default" .}}
60-{{end}}
61-
62-{{template "footer" .}}
50+ {{template "blog-default" .}}
51+ {{template "footer" .}}
6352 {{end}}
+1, -1
......@@ -1,6 +1,6 @@
11 {{define "footer"}}
22 <footer>
33 <hr />
4- published with <a href={{.Site.HomeURL}}>{{.Site.Domain}}</a>
4+ published with <a href="{{.Site.HomeURL}}">{{.Site.Domain}}</a>
55 </footer>
66 {{end}}
+72, -66
......@@ -3,86 +3,92 @@
33 {{define "title"}}{{.PageTitle}}{{end}}
44
55 {{define "meta"}}
6-{{if .Favicon}}
7-<link rel="icon" href="{{.Favicon}}">
8-{{else}}
9-<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10-{{end}}
11-
12-<meta name="description" content="{{.Description}}" />
6+ {{if .Favicon}}
7+ <link rel="icon" href="{{.Favicon}}">
8+ {{else}}
9+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
10+ {{end}}
1311
14-<meta property="og:type" content="website">
15-<meta property="og:site_name" content="{{.Site.Domain}}">
16-<meta property="og:url" content="{{.URL}}">
17-<meta property="og:title" content="{{.Title}}">
18-{{if .Description}}<meta property="og:description" content="{{.Description}}">{{end}}
12+ <meta name="description" content="{{.Description}}" />
1913
20-{{if .Image}}
21-<meta itemprop="image" content="{{.Image}}" />
22-<meta property="og:image" content="{{.Image}}" />
14+ <meta property="og:type" content="website">
15+ <meta property="og:site_name" content="{{.Site.Domain}}">
16+ <meta property="og:url" content="{{.URL}}">
17+ <meta property="og:title" content="{{.Title}}">
18+ {{if .Description}}<meta property="og:description" content="{{.Description}}">{{end}}
2319
24-<meta name="twitter:image" content="{{.Image}}" />
25-{{else}}
26-<meta property="og:image:width" content="300" />
27-<meta property="og:image:height" content="300" />
28-<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
29-<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
30-
31-<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
32-{{end}}
20+ {{if .Image}}
21+ <meta itemprop="image" content="{{.Image}}" />
22+ <meta property="og:image" content="{{.Image}}" />
23+ <meta name="twitter:image" content="{{.Image}}" />
24+ {{else}}
25+ <meta property="og:image:width" content="300" />
26+ <meta property="og:image:height" content="300" />
27+ <meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
28+ <meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
29+ <meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
30+ {{end}}
3331
34-{{if .ImageCard}}
35-<meta property="twitter:card" content="{{.ImageCard}}">
36-{{else}}
37-<meta property="twitter:card" content="summary">
38-{{end}}
39-<meta property="twitter:url" content="{{.URL}}">
40-<meta property="twitter:title" content="{{.Title}}">
41-{{if .Description}}<meta property="twitter:description" content="{{.Description}}">{{end}}
32+ {{if .ImageCard}}
33+ <meta property="twitter:card" content="{{.ImageCard}}">
34+ {{else}}
35+ <meta property="twitter:card" content="summary">
36+ {{end}}
37+ <meta property="twitter:url" content="{{.URL}}">
38+ <meta property="twitter:title" content="{{.Title}}">
39+ {{if .Description}}<meta property="twitter:description" content="{{.Description}}">{{end}}
4240
43-<link rel="stylesheet" href="/syntax.css" />
44-{{if .WithStyles}}
45- <link rel="stylesheet" href="/smol.css" />
46-{{else}}
47-{{end}}
48-{{if .HasCSS}}<link rel="stylesheet" href="{{.CssURL}}" />{{end}}
41+ <link rel="stylesheet" href="/reset.css" />
42+ <link rel="stylesheet" href="/syntax.css" />
43+ {{if .HasCSS}}<link rel="stylesheet" href="{{.CssURL}}" />{{end}}
4944 {{end}}
5045
5146 {{define "attrs"}}id="post" class="{{.Slug}}{{if .Unlisted}} post-draft{{end}}"{{end}}
5247
5348 {{define "body"}}
54-<header>
55- <h1 class="text-2xl font-bold">{{.Title}}</h1>
56- <p class="font-bold m-0">
57- <span id="publish-at">
49+ <main>
50+ <article class="md">
51+ <header>
52+ <h1>{{.Title}}</h1>
53+ <p>
5854 <time datetime="{{.PublishAtISO}}">{{.PublishAt}}</time>
5955 <span>&middot;</span>
60- </span>
61- <a href="{{.BlogURL}}">{{.BlogName}}</a>
62- </p>
63- {{if .Description}}<blockquote>{{.Description}}</blockquote>{{end}}
64- <hr />
65-</header>
66-<main>
67- <article class="md">
68- {{if .List}}
69- {{template "list" .List}}
70- {{else}}
71- {{.Contents}}
72- {{end}}
73-
74- <div class="tags mt-4">
75- {{range .Tags}}
76- <code class="pill tag"><a href="{{$.BlogURL}}?tag={{.}}">#{{.}}</a></code>
56+ <a href="{{.BlogURL}}">{{.BlogName}}</a>
57+ {{if .Description}}
58+ <br />
59+ <span>{{.Description}}</span>
7760 {{end}}
78- </div>
61+ </p>
62+
63+ <hr />
64+ </header>
65+
66+ {{if .List}}
67+ {{template "list" .List}}
68+ {{else}}
69+ {{.Contents}}
70+ {{end}}
7971
80- <div id="last-updated" class="text-sm">
81- last updated: <time datetime="{{.UpdatedAtISO}}">{{.UpdatedAt}}</time>
82- </div>
72+ <footer>
73+ <hr />
74+ {{if or .Tags .UpdatedAt}}
75+ <p>
76+ {{if .Tags}}
77+ {{range .Tags}}
78+ <a href="{{$.BlogURL}}?tag={{.}}">#{{.}}</a>&nbsp;
79+ {{end}}
80+ {{if .UpdatedAt}}<br />{{end}}
81+ {{end}}
82+ {{if .UpdatedAt}}
83+ <span>last updated: <time datetime="{{.UpdatedAtISO}}">{{.UpdatedAt}}</time></span>
84+ {{end}}
85+ </p>
86+ {{end}}
8387
84- <div id="post-footer">{{.Footer}}</div>
88+ {{if .Footer}}<div id="post-footer">{{.Footer}}</div>{{end}}
89+ </footer>
8590 </article>
86-</main>
87-{{template "footer" .}}
91+ </main>
92+
93+ {{template "footer" .}}
8894 {{end}}
+95, -0
......@@ -0,0 +1,95 @@
1+*, *::before, *::after {
2+ box-sizing: border-box;
3+}
4+
5+:is(h1, h2, h3, h4, h5, h6, p, blockquote, hr, ul, ol, pre) + * {
6+ margin-top: 1rem;
7+}
8+
9+html {
10+ color-scheme: light dark;
11+}
12+
13+@media (prefers-reduced-motion: no-preference) {
14+ html {
15+ interpolate-size: allow-keywords;
16+ }
17+}
18+
19+body {
20+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
21+ max-width: 70ch;
22+ margin-inline: auto;
23+ padding-inline: 1rem;
24+ line-height: 1.5;
25+ -webkit-font-smoothing: antialiased;
26+}
27+
28+img, picture, video, canvas, svg {
29+ display: block;
30+ max-width: 100%;
31+ height: auto;
32+}
33+
34+input, button, textarea, select {
35+ font: inherit;
36+}
37+
38+code, kbd, samp, pre {
39+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
40+ font-size: 1em;
41+}
42+
43+pre {
44+ overflow-x: auto;
45+}
46+
47+table {
48+ border-collapse: collapse;
49+ border-spacing: 0;
50+}
51+
52+hr {
53+ border: none;
54+ border-top: 1px solid currentColor;
55+}
56+
57+p, h1, h2, h3, h4, h5, h6 {
58+ overflow-wrap: break-word;
59+}
60+
61+p {
62+ text-wrap: pretty;
63+}
64+
65+h1, h2, h3, h4, h5, h6 {
66+ text-wrap: balance;
67+}
68+
69+blockquote {
70+ border-inline-start: 2px solid currentColor;
71+ padding-inline-start: 1ch;
72+ margin: 0;
73+}
74+
75+[role="list"] {
76+ display: flex;
77+ flex-direction: column;
78+ gap: 0.5rem;
79+ list-style: none;
80+ padding: 0;
81+}
82+
83+[role="list"] [role="list"] {
84+ margin-left: 1.3rem;
85+}
86+
87+[role="listitem"] {
88+ display: flex;
89+ align-items: flex-start;
90+ gap: 5px;
91+}
92+
93+[role="listitem"]:has(pre, img, blockquote) {
94+ align-items: center;
95+}