pico

created pr with pico/141.1 on 2026-09-26T01:18:41Z · by c8ef7d19
added pico/141.2 on 2026-09-26T01:40:03Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
-: ------- > 2: ee606d0 chore(prose): require plus or prose feature flag in order to upload
added pico/141.3 on 2026-09-26T14:41:34Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: ee606d0 = 2: ee606d0 chore(prose): require plus or prose feature flag in order to upload
-: ------- > 3: e14a99e refactor: remove unnecessary tests
added pico/141.4 on 2026-09-26T14:42:29Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: ee606d0 < -: ------- chore(prose): require plus or prose feature flag in order to upload
3: e14a99e ! 2: 7fb4f8f chore(prose): require plus or prose feature flag in order to upload
added pico/141.5 on 2026-09-26T15:11:30Z · by c8ef7d19
1: 2845ffb = 1: 2845ffb refactor: invite system for pgs and prose
2: 7fb4f8f ! 2: e469694 chore(prose): require plus or prose feature flag in order to upload
added pico/141.6 on 2026-10-01T13:39:16Z · by c8ef7d19
1: 2845ffb ! 1: 1e9cddc style(prose): minimal design ethos
2: e469694 ! 2: 0967882 chore(prose): remove with_styles and layout front-matter fields
cmds
checkout latest patchset:
ssh pr.pico.sh pull pico:141 | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull pico:141 [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pico:141
add comment to patch request:
ssh pr.pico.sh comment pico:141 "lgtm!"

Patchset pico/141.6 on 2026-10-01T13:39:16Z · commit 1e9cddc

style(prose): minimal design ethos
Eric Bower 2026-09-30T13:00:35Z
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.
Semantic diff summary
0 added, 2 modified, 0 signature changed, 0 removed across 1 analyzed file (7 files skipped: unsupported file type)
+1 -4 pkg/apps/prose/api.go #
......@@ -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 pkg/apps/prose/html/base.layout.tmpl #
......@@ -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 pkg/apps/prose/html/blog-aside.partial.tmpl #
......@@ -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 pkg/apps/prose/html/blog-default.partial.tmpl #
......@@ -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 pkg/apps/prose/html/blog.page.tmpl #
......@@ -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 pkg/apps/prose/html/footer.partial.tmpl #
......@@ -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 pkg/apps/prose/html/post.page.tmpl #
......@@ -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 pkg/apps/prose/public/reset.css #
......@@ -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+}
Back to top