repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
1e436da
parent
44c4fb4
author
Eric Bower
date
2024-02-17 03:18:04 +0000 UTC
refactor: replace zap with slog (#75)

chore: remove lists project
62 files changed,  +392, -1786
M go.mod
M go.sum
M .github/workflows/build.yml
+1, -1
1@@ -41,7 +41,7 @@ jobs:
2     needs: test
3     strategy:
4       matrix:
5-        APP: [lists, prose, pastes, imgs, pgs, feeds]
6+        APP: [prose, pastes, imgs, pgs, feeds]
7     steps:
8     - name: Checkout repo
9       uses: actions/checkout@v3
M .github/workflows/static.yml
+1, -1
1@@ -22,7 +22,7 @@ jobs:
2           pgit \
3             --out ./public \
4             --label pico \
5-            --desc "pico services - prose.sh, lists.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh" \
6+            --desc "pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh" \
7             --clone-url "https://github.com/picosh/pico.git" \
8             --home-url "https://git.erock.io" \
9             --revs main
M Dockerfile
+3, -3
 1@@ -14,7 +14,7 @@ FROM builder-deps as builder-web
 2 
 3 COPY . .
 4 
 5-ARG APP=lists
 6+ARG APP=prose
 7 ARG TARGETOS
 8 ARG TARGETARCH
 9 
10@@ -33,7 +33,7 @@ FROM scratch as release-web
11 
12 WORKDIR /app
13 
14-ARG APP=lists
15+ARG APP=prose
16 
17 COPY --from=builder-web /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
18 COPY --from=builder-web /go/bin/${APP}-web ./web
19@@ -46,7 +46,7 @@ FROM scratch as release-ssh
20 
21 WORKDIR /app
22 
23-ARG APP=lists
24+ARG APP=prose
25 
26 COPY --from=builder-ssh /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
27 COPY --from=builder-ssh /go/bin/${APP}-ssh ./ssh
M Makefile
+3, -3
 1@@ -44,7 +44,7 @@ bp-%: bp-setup
 2 	$(DOCKER_BUILDX_BUILD) --build-arg "APP=$*" -t "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)" --target release-web .
 3 .PHONY: bp-%
 4 
 5-bp-all: bp-prose bp-lists bp-pastes bp-imgs bp-feeds bp-pgs bp-auth bp-bouncer
 6+bp-all: bp-prose bp-pastes bp-imgs bp-feeds bp-pgs bp-auth bp-bouncer
 7 .PHONY: bp-all
 8 
 9 bp-podman-%:
10@@ -54,7 +54,7 @@ bp-podman-%:
11 	$(DOCKER_CMD) push "ghcr.io/picosh/pico/$*-web:$(DOCKER_TAG)"
12 .PHONY: bp-%
13 
14-bp-podman-all: bp-podman-prose bp-podman-lists bp-podman-pastes bp-podman-imgs bp-podman-feeds bp-podman-pgs
15+bp-podman-all: bp-podman-prose bp-podman-pastes bp-podman-imgs bp-podman-feeds bp-podman-pgs
16 .PHONY: all
17 
18 build-auth:
19@@ -66,7 +66,7 @@ build-%:
20 	go build -o "build/$*-ssh" "./cmd/$*/ssh"
21 .PHONY: build-%
22 
23-build: build-prose build-lists build-pastes build-imgs build-feeds build-pgs build-auth
24+build: build-prose build-pastes build-imgs build-feeds build-pgs build-auth
25 .PHONY: build
26 
27 store-clean:
M auth/auth.go
+41, -20
  1@@ -5,6 +5,7 @@ import (
  2 	"encoding/json"
  3 	"fmt"
  4 	"html/template"
  5+	"log/slog"
  6 	"net/http"
  7 	"net/url"
  8 	"strings"
  9@@ -12,13 +13,12 @@ import (
 10 	"github.com/picosh/pico/db"
 11 	"github.com/picosh/pico/db/postgres"
 12 	"github.com/picosh/pico/shared"
 13-	"go.uber.org/zap"
 14 )
 15 
 16 type Client struct {
 17 	Cfg    *AuthCfg
 18 	Dbpool db.DB
 19-	Logger *zap.SugaredLogger
 20+	Logger *slog.Logger
 21 }
 22 
 23 func (client *Client) hasPrivilegedAccess(apiToken string) bool {
 24@@ -73,7 +73,7 @@ func wellKnownHandler(w http.ResponseWriter, r *http.Request) {
 25 	w.WriteHeader(http.StatusOK)
 26 	err := json.NewEncoder(w).Encode(p)
 27 	if err != nil {
 28-		client.Logger.Error(err)
 29+		client.Logger.Error(err.Error())
 30 		http.Error(w, err.Error(), http.StatusInternalServerError)
 31 	}
 32 }
 33@@ -86,11 +86,11 @@ type oauth2Introspection struct {
 34 func introspectHandler(w http.ResponseWriter, r *http.Request) {
 35 	client := getClient(r)
 36 	token := r.FormValue("token")
 37-	client.Logger.Infof("introspect token (%s)", token)
 38+	client.Logger.Info("introspect token", "token", token)
 39 
 40 	user, err := client.Dbpool.FindUserForToken(token)
 41 	if err != nil {
 42-		client.Logger.Error(err)
 43+		client.Logger.Error(err.Error())
 44 		http.Error(w, err.Error(), http.StatusUnauthorized)
 45 		return
 46 	}
 47@@ -103,7 +103,7 @@ func introspectHandler(w http.ResponseWriter, r *http.Request) {
 48 	w.WriteHeader(http.StatusOK)
 49 	err = json.NewEncoder(w).Encode(p)
 50 	if err != nil {
 51-		client.Logger.Error(err)
 52+		client.Logger.Error(err.Error())
 53 		http.Error(w, err.Error(), http.StatusInternalServerError)
 54 	}
 55 }
 56@@ -116,7 +116,13 @@ func authorizeHandler(w http.ResponseWriter, r *http.Request) {
 57 	redirectURI := r.URL.Query().Get("redirect_uri")
 58 	scope := r.URL.Query().Get("scope")
 59 
 60-	client.Logger.Infof("authorize handler (%s, %s, %s, %s)", responseType, clientID, redirectURI, scope)
 61+	client.Logger.Info(
 62+		"authorize handler",
 63+		"responseType", responseType,
 64+		"clientID", clientID,
 65+		"redirectURI", redirectURI,
 66+		"scope", scope,
 67+	)
 68 
 69 	ts, err := template.ParseFiles(
 70 		"auth/html/redirect.page.tmpl",
 71@@ -126,7 +132,7 @@ func authorizeHandler(w http.ResponseWriter, r *http.Request) {
 72 	)
 73 
 74 	if err != nil {
 75-		client.Logger.Error(err)
 76+		client.Logger.Error(err.Error())
 77 		http.Error(w, err.Error(), http.StatusUnauthorized)
 78 		return
 79 	}
 80@@ -139,7 +145,7 @@ func authorizeHandler(w http.ResponseWriter, r *http.Request) {
 81 	})
 82 
 83 	if err != nil {
 84-		client.Logger.Error(err)
 85+		client.Logger.Error(err.Error())
 86 		http.Error(w, err.Error(), http.StatusUnauthorized)
 87 		return
 88 	}
 89@@ -152,7 +158,11 @@ func redirectHandler(w http.ResponseWriter, r *http.Request) {
 90 	redirectURI := r.FormValue("redirect_uri")
 91 	responseType := r.FormValue("response_type")
 92 
 93-	client.Logger.Infof("redirect handler (%s, %s, %s)", token, redirectURI, responseType)
 94+	client.Logger.Info("redirect handler",
 95+		"token", token,
 96+		"redirectURI", redirectURI,
 97+		"responseType", responseType,
 98+	)
 99 
100 	if token == "" || redirectURI == "" || responseType != "code" {
101 		http.Error(w, "bad request", http.StatusBadRequest)
102@@ -184,11 +194,16 @@ func tokenHandler(w http.ResponseWriter, r *http.Request) {
103 	redirectURI := r.FormValue("redirect_uri")
104 	grantType := r.FormValue("grant_type")
105 
106-	client.Logger.Infof("handle token (%s, %s, %s)", token, redirectURI, grantType)
107+	client.Logger.Info(
108+		"handle token",
109+		"token", token,
110+		"redirectURI", redirectURI,
111+		"grantType", grantType,
112+	)
113 
114 	_, err := client.Dbpool.FindUserForToken(token)
115 	if err != nil {
116-		client.Logger.Error(err)
117+		client.Logger.Error(err.Error())
118 		http.Error(w, err.Error(), http.StatusUnauthorized)
119 		return
120 	}
121@@ -200,7 +215,7 @@ func tokenHandler(w http.ResponseWriter, r *http.Request) {
122 	w.WriteHeader(http.StatusOK)
123 	err = json.NewEncoder(w).Encode(p)
124 	if err != nil {
125-		client.Logger.Error(err)
126+		client.Logger.Error(err.Error())
127 		http.Error(w, err.Error(), http.StatusInternalServerError)
128 	}
129 }
130@@ -218,7 +233,7 @@ func keyHandler(w http.ResponseWriter, r *http.Request) {
131 
132 	err := json.NewDecoder(r.Body).Decode(&data)
133 	if err != nil {
134-		client.Logger.Error(err)
135+		client.Logger.Error(err.Error())
136 		http.Error(w, err.Error(), http.StatusBadRequest)
137 		return
138 	}
139@@ -226,15 +241,21 @@ func keyHandler(w http.ResponseWriter, r *http.Request) {
140 	space := r.URL.Query().Get("space")
141 	if space == "" {
142 		spaceErr := fmt.Errorf("Must provide `space` query parameter")
143-		client.Logger.Error(spaceErr)
144+		client.Logger.Error(spaceErr.Error())
145 		http.Error(w, spaceErr.Error(), http.StatusUnprocessableEntity)
146 	}
147 
148-	client.Logger.Infof("handle key (%s, %s, %s, %s)", data.RemoteAddress, data.Username, space, data.PublicKey)
149+	client.Logger.Info(
150+		"handle key",
151+		"remoteAddress", data.RemoteAddress,
152+		"user", data.Username,
153+		"space", space,
154+		"publicKey", data.PublicKey,
155+	)
156 
157 	user, err := client.Dbpool.FindUserForKey(data.Username, data.PublicKey)
158 	if err != nil {
159-		client.Logger.Error(err)
160+		client.Logger.Error(err.Error())
161 		http.Error(w, err.Error(), http.StatusUnauthorized)
162 		return
163 	}
164@@ -253,7 +274,7 @@ func keyHandler(w http.ResponseWriter, r *http.Request) {
165 	w.WriteHeader(http.StatusOK)
166 	err = json.NewEncoder(w).Encode(user)
167 	if err != nil {
168-		client.Logger.Error(err)
169+		client.Logger.Error(err.Error())
170 		http.Error(w, err.Error(), http.StatusInternalServerError)
171 	}
172 }
173@@ -342,6 +363,6 @@ func StartApiServer() {
174 	router := http.HandlerFunc(handler(routes, client))
175 
176 	portStr := fmt.Sprintf(":%s", cfg.Port)
177-	client.Logger.Infof("Starting server on port %s", cfg.Port)
178-	client.Logger.Fatal(http.ListenAndServe(portStr, router))
179+	client.Logger.Info("starting server on port", "port", cfg.Port)
180+	client.Logger.Error(http.ListenAndServe(portStr, router).Error())
181 }
D cmd/lists/ssh/main.go
+0, -7
1@@ -1,7 +0,0 @@
2-package main
3-
4-import "github.com/picosh/pico/lists"
5-
6-func main() {
7-	lists.StartSshServer()
8-}
D cmd/lists/web/main.go
+0, -7
1@@ -1,7 +0,0 @@
2-package main
3-
4-import "github.com/picosh/pico/lists"
5-
6-func main() {
7-	lists.StartApiServer()
8-}
M cmd/scripts/clean-object-store/clean.go
+5, -15
 1@@ -1,7 +1,7 @@
 2 package main
 3 
 4 import (
 5-	"log"
 6+	"log/slog"
 7 	"os"
 8 	"strings"
 9 
10@@ -11,18 +11,8 @@ import (
11 	"github.com/picosh/pico/shared"
12 	"github.com/picosh/pico/shared/storage"
13 	"github.com/picosh/pico/wish/cms/config"
14-	"go.uber.org/zap"
15 )
16 
17-func createLogger() *zap.SugaredLogger {
18-	logger, err := zap.NewProduction()
19-	if err != nil {
20-		log.Fatal(err)
21-	}
22-
23-	return logger.Sugar()
24-}
25-
26 func bail(err error) {
27 	if err != nil {
28 		panic(err)
29@@ -43,7 +33,7 @@ func main() {
30 	if writeEnv == "1" {
31 		write = true
32 	}
33-	logger := createLogger()
34+	logger := slog.Default()
35 
36 	picoCfg := config.NewConfigCms()
37 	picoCfg.Logger = logger
38@@ -104,7 +94,7 @@ func main() {
39 				}
40 			}
41 			if !found {
42-				logger.Infof("marking (bucket: %s) (%s) for removal", bucketName, bucketProject.Name())
43+				logger.Info("marking for removal", "bucket", bucketName, "project", bucketProject.Name())
44 				rmProjects = append(rmProjects, RmProject{
45 					name: bucketProject.Name(),
46 					user: user,
47@@ -130,9 +120,9 @@ func main() {
48 		bail(err)
49 	}
50 
51-	logger.Infof("(%d) Store projects marked for deletion", len(rmProjects))
52+	logger.Info("store projects marked for deletion", "length", len(rmProjects))
53 	for _, project := range rmProjects {
54-		logger.Infof("(user: %s) (project: %s)", project.user.Name, project.name)
55+		logger.Info("removing project", "user", project.user.Name, "project", project.name)
56 	}
57 	if !write {
58 		logger.Info("WARNING: changes not committed, need env var WRITE=1")
M cmd/scripts/dates/dates.go
+8, -18
 1@@ -3,7 +3,7 @@ package main
 2 import (
 3 	"context"
 4 	"database/sql"
 5-	"log"
 6+	"log/slog"
 7 	"os"
 8 	"time"
 9 
10@@ -11,18 +11,8 @@ import (
11 	"github.com/picosh/pico/db/postgres"
12 	"github.com/picosh/pico/shared"
13 	"github.com/picosh/pico/wish/cms/config"
14-	"go.uber.org/zap"
15 )
16 
17-func createLogger() *zap.SugaredLogger {
18-	logger, err := zap.NewProduction()
19-	if err != nil {
20-		log.Fatal(err)
21-	}
22-
23-	return logger.Sugar()
24-}
25-
26 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
27 	var posts []*db.Post
28 	rs, err := dbpool.Query(`SELECT
29@@ -67,7 +57,7 @@ func updateDates(tx *sql.Tx, postID string, date *time.Time) error {
30 }
31 
32 func main() {
33-	logger := createLogger()
34+	logger := slog.Default()
35 
36 	picoCfg := config.NewConfigCms()
37 	picoCfg.Logger = logger
38@@ -79,7 +69,7 @@ func main() {
39 	if err != nil {
40 		panic(err)
41 	}
42-	logger.Infof("found (%d) posts", len(posts))
43+	logger.Info("found posts", "len", len(posts))
44 
45 	ctx := context.Background()
46 	tx, err := picoDb.Db.BeginTx(ctx, nil)
47@@ -98,14 +88,14 @@ func main() {
48 		if post.Space == "prose" {
49 			parsed, err := shared.ParseText(post.Text)
50 			if err != nil {
51-				logger.Error(err)
52+				logger.Error(err.Error())
53 				continue
54 			}
55 
56 			if parsed.PublishAt != nil && !parsed.PublishAt.IsZero() {
57 				err = updateDates(tx, post.ID, parsed.MetaData.PublishAt)
58 				if err != nil {
59-					logger.Error(err)
60+					logger.Error(err.Error())
61 					continue
62 				}
63 
64@@ -116,14 +106,14 @@ func main() {
65 		} else if post.Space == "lists" {
66 			parsed := shared.ListParseText(post.Text)
67 			if err != nil {
68-				logger.Error(err)
69+				logger.Error(err.Error())
70 				continue
71 			}
72 
73 			if parsed.PublishAt != nil && !parsed.PublishAt.IsZero() {
74 				err = updateDates(tx, post.ID, parsed.PublishAt)
75 				if err != nil {
76-					logger.Error(err)
77+					logger.Error(err.Error())
78 					continue
79 				}
80 				if !parsed.PublishAt.Equal(*post.PublishAt) {
81@@ -137,5 +127,5 @@ func main() {
82 	if err != nil {
83 		panic(err)
84 	}
85-	logger.Infof("(%d) dates fixed!", len(datesFixed))
86+	logger.Info("dates fixed!", "len", len(datesFixed))
87 }
M cmd/scripts/file-size-sync/sync.go
+2, -12
 1@@ -2,23 +2,13 @@ package main
 2 
 3 import (
 4 	"encoding/binary"
 5-	"log"
 6+	"log/slog"
 7 	"os"
 8 
 9 	"github.com/picosh/pico/db/postgres"
10 	"github.com/picosh/pico/wish/cms/config"
11-	"go.uber.org/zap"
12 )
13 
14-func createLogger() *zap.SugaredLogger {
15-	logger, err := zap.NewProduction()
16-	if err != nil {
17-		log.Fatal(err)
18-	}
19-
20-	return logger.Sugar()
21-}
22-
23 func bail(err error) {
24 	if err != nil {
25 		panic(err)
26@@ -26,7 +16,7 @@ func bail(err error) {
27 }
28 
29 func main() {
30-	logger := createLogger()
31+	logger := slog.Default()
32 
33 	picoCfg := config.NewConfigCms()
34 	picoCfg.Logger = logger
M cmd/scripts/migrate/migrate.go
+4, -14
 1@@ -4,24 +4,14 @@ import (
 2 	"context"
 3 	"database/sql"
 4 	"fmt"
 5-	"log"
 6+	"log/slog"
 7 	"os"
 8 
 9 	"github.com/picosh/pico/db"
10 	"github.com/picosh/pico/db/postgres"
11 	"github.com/picosh/pico/wish/cms/config"
12-	"go.uber.org/zap"
13 )
14 
15-func createLogger() *zap.SugaredLogger {
16-	logger, err := zap.NewProduction()
17-	if err != nil {
18-		log.Fatal(err)
19-	}
20-
21-	return logger.Sugar()
22-}
23-
24 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
25 	var posts []*db.Post
26 	rs, err := dbpool.Query(`SELECT
27@@ -109,7 +99,7 @@ type ConflictData struct {
28 }
29 
30 func main() {
31-	logger := createLogger()
32+	logger := slog.Default()
33 
34 	listsCfg := config.NewConfigCms()
35 	listsCfg.Logger = logger
36@@ -221,7 +211,7 @@ func main() {
37 		}
38 	}
39 
40-	logger.Infof("Adding records with no conflicts (%d)", len(noconflicts))
41+	logger.Info("adding records with no conflicts", "len", len(noconflicts))
42 	for _, data := range noconflicts {
43 		err = insertUser(tx, data.User)
44 		if err != nil {
45@@ -236,7 +226,7 @@ func main() {
46 		}
47 	}
48 
49-	logger.Infof("Adding records with conflicts (%d)", len(conflicts))
50+	logger.Info("adding records with conflicts", "len", len(conflicts))
51 	for _, data := range conflicts {
52 		data.User.Name = fmt.Sprintf("%stmp", data.User.Name)
53 		err = insertUser(tx, data.User)
M cmd/scripts/shasum/shasum.go
+3, -13
 1@@ -1,26 +1,16 @@
 2 package main
 3 
 4 import (
 5-	"log"
 6+	"log/slog"
 7 	"os"
 8 
 9 	"github.com/picosh/pico/db/postgres"
10 	"github.com/picosh/pico/shared"
11 	"github.com/picosh/pico/wish/cms/config"
12-	"go.uber.org/zap"
13 )
14 
15-func createLogger() *zap.SugaredLogger {
16-	logger, err := zap.NewProduction()
17-	if err != nil {
18-		log.Fatal(err)
19-	}
20-
21-	return logger.Sugar()
22-}
23-
24 func main() {
25-	logger := createLogger()
26+	logger := slog.Default()
27 	picoCfg := config.NewConfigCms()
28 	picoCfg.Logger = logger
29 	picoCfg.DbURL = os.Getenv("DATABASE_URL")
30@@ -49,5 +39,5 @@ func main() {
31 		}
32 	}
33 
34-	logger.Infof("empty (%d), diff (%d)", empty, diff)
35+	logger.Info("empty, diff", "empty", empty, "diff", diff)
36 }
M cmd/scripts/tags/tags.go
+2, -12
 1@@ -2,25 +2,15 @@ package main
 2 
 3 import (
 4 	"database/sql"
 5-	"log"
 6+	"log/slog"
 7 	"os"
 8 
 9 	"github.com/picosh/pico/db"
10 	"github.com/picosh/pico/db/postgres"
11 	"github.com/picosh/pico/shared"
12 	"github.com/picosh/pico/wish/cms/config"
13-	"go.uber.org/zap"
14 )
15 
16-func createLogger() *zap.SugaredLogger {
17-	logger, err := zap.NewProduction()
18-	if err != nil {
19-		log.Fatal(err)
20-	}
21-
22-	return logger.Sugar()
23-}
24-
25 func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
26 	var posts []*db.Post
27 	rs, err := dbpool.Query(`SELECT
28@@ -60,7 +50,7 @@ func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
29 }
30 
31 func main() {
32-	logger := createLogger()
33+	logger := slog.Default()
34 
35 	picoCfg := config.NewConfigCms()
36 	picoCfg.Logger = logger
M db/postgres/storage.go
+11, -11
 1@@ -5,6 +5,7 @@ import (
 2 	"database/sql"
 3 	"errors"
 4 	"fmt"
 5+	"log/slog"
 6 	"math"
 7 	"strings"
 8 	"time"
 9@@ -14,7 +15,6 @@ import (
10 	_ "github.com/lib/pq"
11 	"github.com/picosh/pico/db"
12 	"github.com/picosh/pico/shared"
13-	"go.uber.org/zap"
14 )
15 
16 var PAGER_SIZE = 15
17@@ -271,7 +271,7 @@ const (
18 )
19 
20 type PsqlDB struct {
21-	Logger *zap.SugaredLogger
22+	Logger *slog.Logger
23 	Db     *sql.DB
24 }
25 
26@@ -347,16 +347,16 @@ func CreatePostWithTagsFromRow(r RowScanner) (*db.Post, error) {
27 	return post, nil
28 }
29 
30-func NewDB(databaseUrl string, logger *zap.SugaredLogger) *PsqlDB {
31+func NewDB(databaseUrl string, logger *slog.Logger) *PsqlDB {
32 	var err error
33 	d := &PsqlDB{
34 		Logger: logger,
35 	}
36-	d.Logger.Infof("Connecting to postgres: %s", databaseUrl)
37+	d.Logger.Info("Connecting to postgres", "databaseUrl", databaseUrl)
38 
39 	db, err := sql.Open("postgres", databaseUrl)
40 	if err != nil {
41-		d.Logger.Fatal(err)
42+		d.Logger.Error(err.Error())
43 	}
44 	d.Db = db
45 	return d
46@@ -507,7 +507,7 @@ func (me *PsqlDB) FindPostsBeforeDate(date *time.Time, space string) ([]*db.Post
47 }
48 
49 func (me *PsqlDB) FindUserForKey(username string, key string) (*db.User, error) {
50-	me.Logger.Infof("Attempting to find user with only public key (%s)", key)
51+	me.Logger.Info("attempting to find user with only public key", "key", key)
52 	pk, err := me.FindPublicKeyForKey(key)
53 	if err == nil {
54 		user, err := me.FindUser(pk.UserID)
55@@ -519,10 +519,10 @@ func (me *PsqlDB) FindUserForKey(username string, key string) (*db.User, error)
56 	}
57 
58 	if errors.Is(err, &db.ErrMultiplePublicKeys{}) {
59-		me.Logger.Infof("Detected multiple users with same public key, using ssh username (%s) to find correct one", username)
60+		me.Logger.Info("detected multiple users with same public key", "user", username)
61 		user, err := me.FindUserForNameAndKey(username, key)
62 		if err != nil {
63-			me.Logger.Infof("Could not find user by username (%s) and public key (%s)", username, key)
64+			me.Logger.Info("could not find user by username and public key", "user", username, "key", key)
65 			// this is a little hacky but if we cannot find a user by name and public key
66 			// then we return the multiple keys detected error so the user knows to specify their
67 			// when logging in
68@@ -997,9 +997,9 @@ func (me *PsqlDB) insertAliasesForPost(tx *sql.Tx, aliases []string, postID stri
69 	ids := make([]string, 0)
70 	for _, alias := range aliases {
71 		if slices.Contains(denyList, alias) {
72-			me.Logger.Infof(
73-				"(%s) is in the deny list for aliases because it conflicts with a static route, skipping",
74-				alias,
75+			me.Logger.Info(
76+				"name is in the deny list for aliases because it conflicts with a static route, skipping",
77+				"alias", alias,
78 			)
79 			continue
80 		}
M feeds/api.go
+8, -6
 1@@ -53,7 +53,7 @@ func StartApiServer() {
 2 	cache := gocache.New(2*time.Minute, 5*time.Minute)
 3 
 4 	if err != nil {
 5-		logger.Fatal(err)
 6+		logger.Error(err.Error())
 7 	}
 8 
 9 	// cron daily digest
10@@ -72,10 +72,12 @@ func StartApiServer() {
11 	router := http.HandlerFunc(handler)
12 
13 	portStr := fmt.Sprintf(":%s", cfg.Port)
14-	logger.Infof("Starting server on port %s", cfg.Port)
15-	logger.Infof("Subdomains enabled: %t", cfg.SubdomainsEnabled)
16-	logger.Infof("Domain: %s", cfg.Domain)
17-	logger.Infof("Email: %s", cfg.Email)
18+	logger.Info(
19+		"Starting server on port",
20+		"port", cfg.Port,
21+		"domain", cfg.Domain,
22+		"email", cfg.Email,
23+	)
24 
25-	logger.Fatal(http.ListenAndServe(portStr, router))
26+	logger.Error(http.ListenAndServe(portStr, router).Error())
27 }
M feeds/cron.go
+18, -18
  1@@ -133,14 +133,14 @@ func (f *Fetcher) Validate(lastDigest *time.Time, parsed *shared.ListParsedText)
  2 }
  3 
  4 func (f *Fetcher) RunPost(user *db.User, post *db.Post) error {
  5-	f.cfg.Logger.Infof("(%s) running feed post (%s)", user.Name, post.Filename)
  6+	f.cfg.Logger.Info("running feed post", "user", user.Name, "filename", post.Filename)
  7 
  8 	parsed := shared.ListParseText(post.Text)
  9 
 10-	f.cfg.Logger.Infof("(%s) Last digest at (%s)", user.Name, post.Data.LastDigest)
 11+	f.cfg.Logger.Info("last digest at", "user", user.Name, "lastDigest", post.Data.LastDigest)
 12 	err := f.Validate(post.Data.LastDigest, parsed)
 13 	if err != nil {
 14-		f.cfg.Logger.Infof("(%s) %s", user.Name, err.Error())
 15+		f.cfg.Logger.Info(err.Error(), "user", user.Name)
 16 		return nil
 17 	}
 18 
 19@@ -184,13 +184,13 @@ func (f *Fetcher) RunUser(user *db.User) error {
 20 	}
 21 
 22 	if len(posts.Data) > 0 {
 23-		f.cfg.Logger.Infof("(%s) found (%d) feed posts", user.Name, len(posts.Data))
 24+		f.cfg.Logger.Info("found feed posts", "user", user.Name, "len", len(posts.Data))
 25 	}
 26 
 27 	for _, post := range posts.Data {
 28 		err = f.RunPost(user, post)
 29 		if err != nil {
 30-			f.cfg.Logger.Infof("(%s) %s", user.Name, err.Error())
 31+			f.cfg.Logger.Info(err.Error(), "user", user)
 32 		}
 33 	}
 34 
 35@@ -232,7 +232,7 @@ func (f *Fetcher) ParseURL(fp *gofeed.Parser, url string) (*gofeed.Feed, error)
 36 }
 37 
 38 func (f *Fetcher) Fetch(fp *gofeed.Parser, url string, username string, feedItems []*db.FeedItem) (*Feed, error) {
 39-	f.cfg.Logger.Infof("(%s) %s fetching feed", username, url)
 40+	f.cfg.Logger.Info("fetching feed", "user", username, "url", url)
 41 
 42 	feed, err := f.ParseURL(fp, url)
 43 	if err != nil {
 44@@ -328,9 +328,9 @@ func (f *Fetcher) FetchAll(urls []string, inlineContent bool, postID string, use
 45 		feedTmpl, err := f.Fetch(fp, url, username, feedItems)
 46 		if err != nil {
 47 			if errors.Is(err, ErrNoRecentArticles) {
 48-				f.cfg.Logger.Info(err)
 49+				f.cfg.Logger.Info(err.Error())
 50 			} else {
 51-				f.cfg.Logger.Error(err)
 52+				f.cfg.Logger.Error(err.Error())
 53 			}
 54 			continue
 55 		}
 56@@ -391,7 +391,7 @@ func (f *Fetcher) SendEmail(username, email string, subject string, msg *MsgBody
 57 	message := mail.NewSingleEmail(from, subject, to, msg.Text, msg.Html)
 58 	client := sendgrid.NewSendClient(f.cfg.SendgridKey)
 59 
 60-	f.cfg.Logger.Infof("(%s) sending email digest", username)
 61+	f.cfg.Logger.Info("sending email digest", "user", username)
 62 	response, err := client.Send(message)
 63 	if err != nil {
 64 		return err
 65@@ -400,15 +400,15 @@ func (f *Fetcher) SendEmail(username, email string, subject string, msg *MsgBody
 66 	// f.cfg.Logger.Infof("(%s) email digest response: %v", username, response)
 67 
 68 	if len(response.Headers["X-Message-Id"]) > 0 {
 69-		f.cfg.Logger.Infof(
 70-			"(%s) successfully sent email digest (x-message-id: %s)",
 71-			email,
 72-			response.Headers["X-Message-Id"][0],
 73+		f.cfg.Logger.Info(
 74+			"successfully sent email digest",
 75+			"email", email,
 76+			"x-message-id", response.Headers["X-Message-Id"][0],
 77 		)
 78 	} else {
 79-		f.cfg.Logger.Errorf(
 80-			"(%s) could not find x-message-id, which means sending an email failed",
 81-			email,
 82+		f.cfg.Logger.Error(
 83+			"could not find x-message-id, which means sending an email failed",
 84+			"email", email,
 85 		)
 86 	}
 87 
 88@@ -424,7 +424,7 @@ func (f *Fetcher) Run() error {
 89 	for _, user := range users {
 90 		err := f.RunUser(user)
 91 		if err != nil {
 92-			f.cfg.Logger.Error(err)
 93+			f.cfg.Logger.Error(err.Error())
 94 			continue
 95 		}
 96 	}
 97@@ -438,7 +438,7 @@ func (f *Fetcher) Loop() {
 98 
 99 		err := f.Run()
100 		if err != nil {
101-			f.cfg.Logger.Error(err)
102+			f.cfg.Logger.Error(err.Error())
103 		}
104 
105 		f.cfg.Logger.Info("digest emailer finished, waiting 10 mins")
M feeds/ssh.go
+7, -5
 1@@ -81,7 +81,8 @@ func StartSshServer() {
 2 	}
 3 
 4 	if err != nil {
 5-		logger.Fatal(err)
 6+		logger.Error(err.Error())
 7+		return
 8 	}
 9 
10 	fileMap := map[string]filehandlers.ReadWriteHandler{
11@@ -100,15 +101,16 @@ func StartSshServer() {
12 		),
13 	)
14 	if err != nil {
15-		logger.Fatal(err)
16+		logger.Error(err.Error())
17+		return
18 	}
19 
20 	done := make(chan os.Signal, 1)
21 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
22-	logger.Infof("Starting SSH server on %s:%s", host, port)
23+	logger.Info("Starting SSH server", "host", host, "port", port)
24 	go func() {
25 		if err = s.ListenAndServe(); err != nil {
26-			logger.Fatal(err)
27+			logger.Error(err.Error())
28 		}
29 	}()
30 
31@@ -117,6 +119,6 @@ func StartSshServer() {
32 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
33 	defer func() { cancel() }()
34 	if err := s.Shutdown(ctx); err != nil {
35-		logger.Fatal(err)
36+		logger.Error(err.Error())
37 	}
38 }
M filehandlers/assets/handler.go
+15, -12
  1@@ -5,6 +5,7 @@ import (
  2 	"encoding/binary"
  3 	"fmt"
  4 	"io"
  5+	"log/slog"
  6 	"os"
  7 	"path/filepath"
  8 	"strings"
  9@@ -17,7 +18,6 @@ import (
 10 	"github.com/picosh/pico/shared/storage"
 11 	"github.com/picosh/pico/wish/cms/util"
 12 	"github.com/picosh/send/send/utils"
 13-	"go.uber.org/zap"
 14 )
 15 
 16 type ctxBucketKey struct{}
 17@@ -81,7 +81,7 @@ func NewUploadAssetHandler(dbpool db.DB, cfg *shared.ConfigSite, storage storage
 18 	}
 19 }
 20 
 21-func (h *UploadAssetHandler) GetLogger() *zap.SugaredLogger {
 22+func (h *UploadAssetHandler) GetLogger() *slog.Logger {
 23 	return h.Cfg.Logger
 24 }
 25 
 26@@ -207,9 +207,9 @@ func (h *UploadAssetHandler) Validate(s ssh.Session) error {
 27 		return err
 28 	}
 29 	s.Context().SetValue(ctxStorageSizeKey{}, totalStorageSize)
 30-	h.Cfg.Logger.Infof("(%s) bucket size is current (%d bytes)", user.Name, totalStorageSize)
 31+	h.Cfg.Logger.Info("bucket size is current (%d bytes)", "user", user.Name, "size", fmt.Sprintf("%d bytes", totalStorageSize))
 32 
 33-	h.Cfg.Logger.Infof("(%s) attempting to upload files to (%s)", user.Name, h.Cfg.Space)
 34+	h.Cfg.Logger.Info("attempting to upload files", "user", user.Name, "space", h.Cfg.Space)
 35 
 36 	return nil
 37 }
 38@@ -217,7 +217,7 @@ func (h *UploadAssetHandler) Validate(s ssh.Session) error {
 39 func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
 40 	user, err := futil.GetUser(s)
 41 	if err != nil {
 42-		h.Cfg.Logger.Error(err)
 43+		h.Cfg.Logger.Error(err.Error())
 44 		return "", err
 45 	}
 46 
 47@@ -232,7 +232,7 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
 48 
 49 	bucket, err := getBucket(s)
 50 	if err != nil {
 51-		h.Cfg.Logger.Error(err)
 52+		h.Cfg.Logger.Error(err.Error())
 53 		return "", err
 54 	}
 55 
 56@@ -245,18 +245,18 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
 57 		if err == nil {
 58 			err = h.DBPool.UpdateProject(user.ID, projectName)
 59 			if err != nil {
 60-				h.Cfg.Logger.Error(err)
 61+				h.Cfg.Logger.Error(err.Error())
 62 				return "", err
 63 			}
 64 		} else {
 65 			_, err = h.DBPool.InsertProject(user.ID, projectName, projectName)
 66 			if err != nil {
 67-				h.Cfg.Logger.Error(err)
 68+				h.Cfg.Logger.Error(err.Error())
 69 				return "", err
 70 			}
 71 			project, err = h.DBPool.FindProjectByName(user.ID, projectName)
 72 			if err != nil {
 73-				h.Cfg.Logger.Error(err)
 74+				h.Cfg.Logger.Error(err.Error())
 75 				return "", err
 76 			}
 77 		}
 78@@ -285,7 +285,7 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *utils.FileEntry) (strin
 79 	}
 80 	err = h.writeAsset(data)
 81 	if err != nil {
 82-		h.Cfg.Logger.Error(err)
 83+		h.Cfg.Logger.Error(err.Error())
 84 		return "", err
 85 	}
 86 	nextStorageSize := incrementStorageSize(s, deltaFileSize)
 87@@ -380,10 +380,13 @@ func (h *UploadAssetHandler) writeAsset(data *FileData) error {
 88 	} else {
 89 		reader := bytes.NewReader(data.Text)
 90 
 91-		h.Cfg.Logger.Infof(
 92-			"(%s) uploading to (bucket: %s) (%s)",
 93+		h.Cfg.Logger.Info(
 94+			"uploading file to bucket",
 95+			"user",
 96 			data.User.Name,
 97+			"bucket",
 98 			data.Bucket.Name,
 99+			"filename",
100 			assetFilename,
101 		)
102 
M filehandlers/imgs/handler.go
+10, -10
 1@@ -49,14 +49,14 @@ func NewUploadImgHandler(dbpool db.DB, cfg *shared.ConfigSite, storage storage.O
 2 func (h *UploadImgHandler) removePost(data *PostMetaData) error {
 3 	// skip empty files from being added to db
 4 	if data.Post == nil {
 5-		h.Cfg.Logger.Infof("(%s) is empty, skipping record", data.Filename)
 6+		h.Cfg.Logger.Info("file is empty, skipping record", "filename", data.Filename)
 7 		return nil
 8 	}
 9 
10-	h.Cfg.Logger.Infof("(%s) is empty, removing record (%s)", data.Filename, data.Cur.ID)
11+	h.Cfg.Logger.Info("file is empty, removing record", "filename", data.Filename, "recordId", data.Cur.ID)
12 	err := h.DBPool.RemovePosts([]string{data.Cur.ID})
13 	if err != nil {
14-		h.Cfg.Logger.Errorf("error for %s: %v", data.Filename, err)
15+		h.Cfg.Logger.Error(err.Error(), "filename", data.Filename)
16 		return fmt.Errorf("error for %s: %v", data.Filename, err)
17 	}
18 
19@@ -105,7 +105,7 @@ func (h *UploadImgHandler) Read(s ssh.Session, entry *utils.FileEntry) (os.FileI
20 func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string, error) {
21 	user, err := util.GetUser(s)
22 	if err != nil {
23-		h.Cfg.Logger.Error(err)
24+		h.Cfg.Logger.Error(err.Error())
25 		return "", err
26 	}
27 
28@@ -125,13 +125,13 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
29 		noExifBytes, err := exifremove.Remove(text)
30 		if err == nil {
31 			if len(noExifBytes) == 0 {
32-				h.Cfg.Logger.Infof("(%s) silently failed to strip exif data", filename)
33+				h.Cfg.Logger.Info("file silently failed to strip exif data", "filename", filename)
34 			} else {
35 				text = noExifBytes
36-				h.Cfg.Logger.Infof("(%s) stripped exif data", filename)
37+				h.Cfg.Logger.Info("stripped exif data", "filename", filename)
38 			}
39 		} else {
40-			h.Cfg.Logger.Error(err)
41+			h.Cfg.Logger.Error(err.Error())
42 		}
43 	}
44 
45@@ -156,7 +156,7 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
46 		Space,
47 	)
48 	if err != nil {
49-		h.Cfg.Logger.Infof("(%s) unable to find image (%s), continuing", nextPost.Filename, err)
50+		h.Cfg.Logger.Info("unable to find image, continuing", "filename", nextPost.Filename, "err", err.Error())
51 	}
52 
53 	featureFlag, err := util.GetFeatureFlag(s)
54@@ -178,13 +178,13 @@ func (h *UploadImgHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
55 
56 	err = h.writeImg(s, &metadata)
57 	if err != nil {
58-		h.Cfg.Logger.Error(err)
59+		h.Cfg.Logger.Error(err.Error())
60 		return "", err
61 	}
62 
63 	totalFileSize, err := h.DBPool.FindTotalSizeForUser(user.ID)
64 	if err != nil {
65-		h.Cfg.Logger.Error(err)
66+		h.Cfg.Logger.Error(err.Error())
67 		return "", err
68 	}
69 
M filehandlers/imgs/img.go
+18, -14
 1@@ -87,11 +87,15 @@ func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error {
 2 
 3 	err = h.metaImg(data)
 4 	if err != nil {
 5-		h.Cfg.Logger.Info(err)
 6+		h.Cfg.Logger.Info(err.Error())
 7 		return err
 8 	}
 9 
10 	modTime := time.Unix(data.Mtime, 0)
11+	logger := h.Cfg.Logger.With(
12+		"user", data.Username,
13+		"filename", data.Filename,
14+	)
15 
16 	if len(data.OrigText) == 0 {
17 		err = h.removePost(data)
18@@ -108,7 +112,7 @@ func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error {
19 			return err
20 		}
21 	} else if data.Cur == nil {
22-		h.Cfg.Logger.Infof("(%s) not found, adding record", data.Filename)
23+		logger.Info("file not found, adding record")
24 		insertPost := db.Post{
25 			UserID: user.ID,
26 			Space:  Space,
27@@ -128,28 +132,28 @@ func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error {
28 		}
29 		_, err := h.DBPool.InsertPost(&insertPost)
30 		if err != nil {
31-			h.Cfg.Logger.Errorf("error for %s: %v", data.Filename, err)
32+			logger.Error(err.Error())
33 			return fmt.Errorf("error for %s: %v", data.Filename, err)
34 		}
35 
36 		if len(data.Tags) > 0 {
37-			h.Cfg.Logger.Infof(
38-				"Found (%s) post tags, replacing with old tags",
39-				strings.Join(data.Tags, ","),
40+			logger.Info(
41+				"found post tags, replacing with old tags",
42+				"tags", strings.Join(data.Tags, ","),
43 			)
44 			err = h.DBPool.ReplaceTagsForPost(data.Tags, data.Post.ID)
45 			if err != nil {
46-				h.Cfg.Logger.Errorf("error for %s: %v", data.Filename, err)
47+				logger.Error(err.Error())
48 				return fmt.Errorf("error for %s: %v", data.Filename, err)
49 			}
50 		}
51 	} else {
52 		if data.Shasum == data.Cur.Shasum && modTime.Equal(*data.Cur.UpdatedAt) {
53-			h.Cfg.Logger.Infof("(%s) found, but image is identical, skipping", data.Filename)
54+			logger.Info("image found, but image is identical, skipping")
55 			return nil
56 		}
57 
58-		h.Cfg.Logger.Infof("(%s) found, updating record", data.Filename)
59+		logger.Info("file found, updating record")
60 
61 		updatePost := db.Post{
62 			ID: data.Cur.ID,
63@@ -167,17 +171,17 @@ func (h *UploadImgHandler) writeImg(s ssh.Session, data *PostMetaData) error {
64 		}
65 		_, err = h.DBPool.UpdatePost(&updatePost)
66 		if err != nil {
67-			h.Cfg.Logger.Errorf("error for %s: %v", data.Filename, err)
68+			logger.Error(err.Error())
69 			return fmt.Errorf("error for %s: %v", data.Filename, err)
70 		}
71 
72-		h.Cfg.Logger.Infof(
73-			"Found (%s) post tags, replacing with old tags",
74-			strings.Join(data.Tags, ","),
75+		logger.Info(
76+			"found post tags, replacing with old tags",
77+			"tags", strings.Join(data.Tags, ","),
78 		)
79 		err = h.DBPool.ReplaceTagsForPost(data.Tags, data.Cur.ID)
80 		if err != nil {
81-			h.Cfg.Logger.Errorf("error for %s: %v", data.Filename, err)
82+			logger.Error(err.Error())
83 			return fmt.Errorf("error for %s: %v", data.Filename, err)
84 		}
85 	}
M filehandlers/post_handler.go
+32, -28
  1@@ -78,12 +78,16 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
  2 	logger := h.Cfg.Logger
  3 	user, err := util.GetUser(s)
  4 	if err != nil {
  5-		logger.Error(err)
  6+		logger.Error(err.Error())
  7 		return "", err
  8 	}
  9 
 10 	userID := user.ID
 11 	filename := filepath.Base(entry.Filepath)
 12+	logger = logger.With(
 13+		"user", user.Name,
 14+		"filename", filename,
 15+	)
 16 
 17 	var origText []byte
 18 	if b, err := io.ReadAll(entry.Reader); err == nil {
 19@@ -120,14 +124,13 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
 20 
 21 	valid, err := h.Hooks.FileValidate(s, &metadata)
 22 	if !valid {
 23-		logger.Error(err)
 24+		logger.Error(err.Error())
 25 		return "", err
 26 	}
 27 
 28 	post, err := h.DBPool.FindPostWithFilename(metadata.Filename, metadata.User.ID, h.Cfg.Space)
 29 	if err != nil {
 30-		logger.Infof("unable to load post (%s), continuing", filename)
 31-		logger.Info(err)
 32+		logger.Info("unable to load post, continuing", "err", err.Error())
 33 	}
 34 
 35 	if post != nil {
 36@@ -137,7 +140,7 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
 37 
 38 	err = h.Hooks.FileMeta(s, &metadata)
 39 	if err != nil {
 40-		logger.Error(err)
 41+		logger.Error(err.Error())
 42 		return "", err
 43 	}
 44 
 45@@ -147,18 +150,18 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
 46 	if len(origText) == 0 {
 47 		// skip empty files from being added to db
 48 		if post == nil {
 49-			logger.Infof("(%s) is empty, skipping record", filename)
 50+			logger.Info("file is empty, skipping record")
 51 			return "", nil
 52 		}
 53 
 54 		err := h.DBPool.RemovePosts([]string{post.ID})
 55-		logger.Infof("(%s) is empty, removing record", filename)
 56+		logger.Info("file is empty, removing record")
 57 		if err != nil {
 58-			logger.Errorf("error for %s: %v", filename, err)
 59+			logger.Error(err.Error())
 60 			return "", fmt.Errorf("error for %s: %v", filename, err)
 61 		}
 62 	} else if post == nil {
 63-		logger.Infof("(%s) not found, adding record", filename)
 64+		logger.Info("file not found, adding record")
 65 		insertPost := db.Post{
 66 			UserID: userID,
 67 			Space:  h.Cfg.Space,
 68@@ -179,41 +182,42 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
 69 		}
 70 		post, err = h.DBPool.InsertPost(&insertPost)
 71 		if err != nil {
 72-			logger.Errorf("error for %s: %v", filename, err)
 73+			logger.Error(err.Error())
 74 			return "", fmt.Errorf("error for %s: %v", filename, err)
 75 		}
 76 
 77 		if len(metadata.Aliases) > 0 {
 78-			logger.Infof(
 79-				"Found (%s) post aliases, replacing with old aliases",
 80+			logger.Info(
 81+				"found post aliases, replacing with old aliases",
 82+				"aliases",
 83 				strings.Join(metadata.Aliases, ","),
 84 			)
 85 			err = h.DBPool.ReplaceAliasesForPost(metadata.Aliases, post.ID)
 86 			if err != nil {
 87-				logger.Errorf("error for %s: %v", filename, err)
 88+				logger.Error(err.Error())
 89 				return "", fmt.Errorf("error for %s: %v", filename, err)
 90 			}
 91 		}
 92 
 93 		if len(metadata.Tags) > 0 {
 94-			logger.Infof(
 95-				"Found (%s) post tags, replacing with old tags",
 96-				strings.Join(metadata.Tags, ","),
 97+			logger.Info(
 98+				"found post tags, replacing with old tags",
 99+				"tags", strings.Join(metadata.Tags, ","),
100 			)
101 			err = h.DBPool.ReplaceTagsForPost(metadata.Tags, post.ID)
102 			if err != nil {
103-				logger.Errorf("error for %s: %v", filename, err)
104+				logger.Error(err.Error())
105 				return "", fmt.Errorf("error for %s: %v", filename, err)
106 			}
107 		}
108 	} else {
109 		if metadata.Text == post.Text && modTime.Equal(*post.UpdatedAt) {
110-			logger.Infof("(%s) found, but text is identical, skipping", filename)
111+			logger.Info("file found, but text is identical, skipping")
112 			curl := shared.NewCreateURL(h.Cfg)
113 			return h.Cfg.FullPostURL(curl, user.Name, metadata.Slug), nil
114 		}
115 
116-		logger.Infof("(%s) found, updating record", filename)
117+		logger.Info("file found, updating record")
118 
119 		updatePost := db.Post{
120 			ID: post.ID,
121@@ -232,27 +236,27 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
122 		}
123 		_, err = h.DBPool.UpdatePost(&updatePost)
124 		if err != nil {
125-			logger.Errorf("error for %s: %v", filename, err)
126+			logger.Error(err.Error())
127 			return "", fmt.Errorf("error for %s: %v", filename, err)
128 		}
129 
130-		logger.Infof(
131-			"Found (%s) post tags, replacing with old tags",
132-			strings.Join(metadata.Tags, ","),
133+		logger.Info(
134+			"found post tags, replacing with old tags",
135+			"tags", strings.Join(metadata.Tags, ","),
136 		)
137 		err = h.DBPool.ReplaceTagsForPost(metadata.Tags, post.ID)
138 		if err != nil {
139-			logger.Errorf("error for %s: %v", filename, err)
140+			logger.Error(err.Error())
141 			return "", fmt.Errorf("error for %s: %v", filename, err)
142 		}
143 
144-		logger.Infof(
145-			"Found (%s) post aliases, replacing with old aliases",
146-			strings.Join(metadata.Aliases, ","),
147+		logger.Info(
148+			"found post aliases, replacing with old aliases",
149+			"aliases", strings.Join(metadata.Aliases, ","),
150 		)
151 		err = h.DBPool.ReplaceAliasesForPost(metadata.Aliases, post.ID)
152 		if err != nil {
153-			logger.Errorf("error for %s: %v", filename, err)
154+			logger.Error(err.Error())
155 			return "", fmt.Errorf("error for %s: %v", filename, err)
156 		}
157 	}
M filehandlers/router_handler.go
+3, -3
 1@@ -2,6 +2,7 @@ package filehandlers
 2 
 3 import (
 4 	"fmt"
 5+	"log/slog"
 6 	"os"
 7 	"path/filepath"
 8 
 9@@ -10,7 +11,6 @@ import (
10 	"github.com/picosh/pico/filehandlers/util"
11 	"github.com/picosh/pico/shared"
12 	"github.com/picosh/send/send/utils"
13-	"go.uber.org/zap"
14 )
15 
16 type ReadWriteHandler interface {
17@@ -125,7 +125,7 @@ func (r *FileHandlerRouter) List(s ssh.Session, fpath string, isDir bool, recurs
18 	return fileList, nil
19 }
20 
21-func (r *FileHandlerRouter) GetLogger() *zap.SugaredLogger {
22+func (r *FileHandlerRouter) GetLogger() *slog.Logger {
23 	return r.Cfg.Logger
24 }
25 
26@@ -163,6 +163,6 @@ func (r *FileHandlerRouter) Validate(s ssh.Session) error {
27 	util.SetUser(s, user)
28 	util.SetFeatureFlag(s, ff)
29 
30-	r.Cfg.Logger.Infof("(%s) attempting to upload files to (%s)", user.Name, r.Cfg.Space)
31+	r.Cfg.Logger.Info("attempting to upload files", "user", user.Name, "space", r.Cfg.Space)
32 	return nil
33 }
M go.mod
+1, -3
 1@@ -21,13 +21,12 @@ require (
 2 	github.com/muesli/reflow v0.3.0
 3 	github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577
 4 	github.com/patrickmn/go-cache v2.1.0+incompatible
 5-	github.com/picosh/send v0.0.0-20231126163457-97725d2b2be1
 6+	github.com/picosh/send v0.0.0-20240217010313-c282075fbdf8
 7 	github.com/sendgrid/sendgrid-go v3.13.0+incompatible
 8 	github.com/yuin/goldmark v1.6.0
 9 	github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
10 	github.com/yuin/goldmark-meta v1.1.0
11 	go.abhg.dev/goldmark/anchor v0.1.1
12-	go.uber.org/zap v1.26.0
13 	golang.org/x/crypto v0.17.0
14 	gopkg.in/yaml.v2 v2.4.0
15 )
16@@ -106,7 +105,6 @@ require (
17 	github.com/tklauser/go-sysconf v0.3.12 // indirect
18 	github.com/tklauser/numcpus v0.6.1 // indirect
19 	github.com/yusufpapurcu/wmi v1.2.3 // indirect
20-	go.uber.org/multierr v1.11.0 // indirect
21 	golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
22 	golang.org/x/net v0.18.0 // indirect
23 	golang.org/x/sync v0.5.0 // indirect
M go.sum
+2, -8
 1@@ -182,8 +182,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
 2 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 3 github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
 4 github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
 5-github.com/picosh/send v0.0.0-20231126163457-97725d2b2be1 h1:CsRnWhDufv7yiLgbClZGvWk/iFRB/8Uzo8jypLfY8J8=
 6-github.com/picosh/send v0.0.0-20231126163457-97725d2b2be1/go.mod h1:3N0Z4Z8367ikx3v14CVU8HCwNXTwz+Brt+GzYE9i8wU=
 7+github.com/picosh/send v0.0.0-20240217010313-c282075fbdf8 h1:yYsjCSE+SRMDVj7efPu4I048jevI42ZvaD0GQiRiRBI=
 8+github.com/picosh/send v0.0.0-20240217010313-c282075fbdf8/go.mod h1:1JCq0NVOdTDenQ0/Kd8e4rP80lu06UHJJ+6dQxhcpew=
 9 github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
10 github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
11 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
12@@ -254,12 +254,6 @@ github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFi
13 github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
14 go.abhg.dev/goldmark/anchor v0.1.1 h1:NUH3hAzhfeymRqZKOkSoFReZlEAmfXBZlbXEzpD2Qgc=
15 go.abhg.dev/goldmark/anchor v0.1.1/go.mod h1:zYKiaHXTdugwVJRZqInVdmNGQRM3ZRJ6AGBC7xP7its=
16-go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
17-go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
18-go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
19-go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
20-go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
21-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
22 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
23 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
24 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
M imgs/api.go
+19, -17
  1@@ -42,7 +42,7 @@ func ImgsListHandler(w http.ResponseWriter, r *http.Request) {
  2 
  3 	user, err := dbpool.FindUserForName(username)
  4 	if err != nil {
  5-		logger.Infof("blog not found: %s", username)
  6+		logger.Info("blog not found", "username", username)
  7 		http.Error(w, "blog not found", http.StatusNotFound)
  8 		return
  9 	}
 10@@ -53,7 +53,7 @@ func ImgsListHandler(w http.ResponseWriter, r *http.Request) {
 11 	posts = p.Data
 12 
 13 	if err != nil {
 14-		logger.Error(err)
 15+		logger.Error(err.Error())
 16 		http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
 17 		return
 18 	}
 19@@ -63,7 +63,7 @@ func ImgsListHandler(w http.ResponseWriter, r *http.Request) {
 20 	})
 21 
 22 	if err != nil {
 23-		logger.Error(err)
 24+		logger.Error(err.Error())
 25 		http.Error(w, err.Error(), http.StatusInternalServerError)
 26 		return
 27 	}
 28@@ -85,7 +85,7 @@ func ImgsListHandler(w http.ResponseWriter, r *http.Request) {
 29 
 30 	err = ts.Execute(w, data)
 31 	if err != nil {
 32-		logger.Error(err)
 33+		logger.Error(err.Error())
 34 		http.Error(w, err.Error(), http.StatusInternalServerError)
 35 	}
 36 }
 37@@ -97,14 +97,14 @@ func ImgsRssHandler(w http.ResponseWriter, r *http.Request) {
 38 
 39 	pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, Space)
 40 	if err != nil {
 41-		logger.Error(err)
 42+		logger.Error(err.Error())
 43 		http.Error(w, err.Error(), http.StatusInternalServerError)
 44 		return
 45 	}
 46 
 47 	ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
 48 	if err != nil {
 49-		logger.Error(err)
 50+		logger.Error(err.Error())
 51 		http.Error(w, err.Error(), http.StatusInternalServerError)
 52 		return
 53 	}
 54@@ -155,14 +155,14 @@ func ImgsRssHandler(w http.ResponseWriter, r *http.Request) {
 55 
 56 	rss, err := feed.ToAtom()
 57 	if err != nil {
 58-		logger.Fatal(err)
 59+		logger.Error(err.Error())
 60 		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
 61 	}
 62 
 63 	w.Header().Add("Content-Type", "application/atom+xml")
 64 	_, err = w.Write([]byte(rss))
 65 	if err != nil {
 66-		logger.Error(err)
 67+		logger.Error(err.Error())
 68 	}
 69 }
 70 
 71@@ -175,7 +175,7 @@ func ImgRequest(w http.ResponseWriter, r *http.Request) {
 72 
 73 	user, err := dbpool.FindUserForName(username)
 74 	if err != nil {
 75-		logger.Infof("rss feed not found: %s", username)
 76+		logger.Info("rss feed not found", "user", username)
 77 		http.Error(w, "rss feed not found", http.StatusNotFound)
 78 		return
 79 	}
 80@@ -193,7 +193,7 @@ func ImgRequest(w http.ResponseWriter, r *http.Request) {
 81 	opts, err := storage.UriToImgProcessOpts(imgOpts)
 82 	if err != nil {
 83 		errMsg := fmt.Sprintf("error processing img options: %s", err.Error())
 84-		logger.Infof(errMsg)
 85+		logger.Info(errMsg)
 86 		http.Error(w, errMsg, http.StatusUnprocessableEntity)
 87 		return
 88 	}
 89@@ -224,7 +224,7 @@ func ImgRequest(w http.ResponseWriter, r *http.Request) {
 90 	post, err := FindImgPost(r, user, slug)
 91 	if err != nil {
 92 		errMsg := fmt.Sprintf("image not found %s/%s", user.Name, slug)
 93-		logger.Infof(errMsg)
 94+		logger.Info(errMsg)
 95 		http.Error(w, errMsg, http.StatusNotFound)
 96 		return
 97 	}
 98@@ -313,7 +313,7 @@ func StartApiServer() {
 99 	cache := gocache.New(2*time.Minute, 5*time.Minute)
100 
101 	if err != nil {
102-		logger.Fatal(err)
103+		logger.Error(err.Error())
104 	}
105 
106 	staticRoutes := []shared.Route{}
107@@ -328,10 +328,12 @@ func StartApiServer() {
108 	router := http.HandlerFunc(handler)
109 
110 	portStr := fmt.Sprintf(":%s", cfg.Port)
111-	logger.Infof("Starting server on port %s", cfg.Port)
112-	logger.Infof("Subdomains enabled: %t", cfg.SubdomainsEnabled)
113-	logger.Infof("Domain: %s", cfg.Domain)
114-	logger.Infof("Email: %s", cfg.Email)
115+	logger.Info(
116+		"Starting server on port",
117+		"port", cfg.Port,
118+		"domain", cfg.Domain,
119+		"email", cfg.Email,
120+	)
121 
122-	logger.Fatal(http.ListenAndServe(portStr, router))
123+	logger.Error(http.ListenAndServe(portStr, router).Error())
124 }
D lists/api.go
+0, -743
  1@@ -1,743 +0,0 @@
  2-package lists
  3-
  4-import (
  5-	"bytes"
  6-	"fmt"
  7-	"html/template"
  8-	"net/http"
  9-	"net/url"
 10-	"sort"
 11-	"strconv"
 12-	"time"
 13-
 14-	"slices"
 15-
 16-	"github.com/gorilla/feeds"
 17-	gocache "github.com/patrickmn/go-cache"
 18-	"github.com/picosh/pico/db"
 19-	"github.com/picosh/pico/db/postgres"
 20-	"github.com/picosh/pico/shared"
 21-	"github.com/picosh/pico/shared/storage"
 22-)
 23-
 24-type PostItemData struct {
 25-	URL            template.URL
 26-	BlogURL        template.URL
 27-	Username       string
 28-	Title          string
 29-	Description    string
 30-	PublishAtISO   string
 31-	PublishAt      string
 32-	UpdatedAtISO   string
 33-	UpdatedTimeAgo string
 34-	Padding        string
 35-}
 36-
 37-type BlogPageData struct {
 38-	Site      shared.SitePageData
 39-	PageTitle string
 40-	URL       template.URL
 41-	RSSURL    template.URL
 42-	Username  string
 43-	Readme    *ReadmeTxt
 44-	Header    *HeaderTxt
 45-	Posts     []PostItemData
 46-	HasFilter bool
 47-}
 48-
 49-type ReadPageData struct {
 50-	Site      shared.SitePageData
 51-	NextPage  string
 52-	PrevPage  string
 53-	Posts     []PostItemData
 54-	Tags      []string
 55-	HasFilter bool
 56-}
 57-
 58-type PostPageData struct {
 59-	Site         shared.SitePageData
 60-	PageTitle    string
 61-	URL          template.URL
 62-	BlogURL      template.URL
 63-	Title        string
 64-	Description  string
 65-	Username     string
 66-	BlogName     string
 67-	ListType     string
 68-	Items        []*shared.ListItem
 69-	PublishAtISO string
 70-	PublishAt    string
 71-	Tags         []string
 72-}
 73-
 74-type TransparencyPageData struct {
 75-	Site      shared.SitePageData
 76-	Analytics *db.Analytics
 77-}
 78-
 79-type HeaderTxt struct {
 80-	Title    string
 81-	Bio      string
 82-	Nav      []*shared.ListItem
 83-	Layout   string
 84-	HasItems bool
 85-}
 86-
 87-type ReadmeTxt struct {
 88-	HasItems bool
 89-	ListType string
 90-	Items    []*shared.ListItem
 91-}
 92-
 93-func getPostsForUser(r *http.Request, user *db.User, tag string, num int) ([]*db.Post, error) {
 94-	dbpool := shared.GetDB(r)
 95-	cfg := shared.GetCfg(r)
 96-	var err error
 97-
 98-	pager := &db.Pager{Num: num, Page: 0}
 99-	var p *db.Paginate[*db.Post]
100-	if tag == "" {
101-		p, err = dbpool.FindPostsForUser(pager, user.ID, cfg.Space)
102-	} else {
103-		p, err = dbpool.FindUserPostsByTag(pager, tag, user.ID, cfg.Space)
104-	}
105-	posts := p.Data
106-
107-	if err != nil {
108-		return posts, err
109-	}
110-
111-	sort.Slice(posts, func(i, j int) bool {
112-		return posts[i].UpdatedAt.After(*posts[j].UpdatedAt)
113-	})
114-
115-	return posts, nil
116-}
117-
118-func isRequestTrackable(r *http.Request) bool {
119-	return true
120-}
121-
122-func blogHandler(w http.ResponseWriter, r *http.Request) {
123-	username := shared.GetUsernameFromRequest(r)
124-	dbpool := shared.GetDB(r)
125-	logger := shared.GetLogger(r)
126-	cfg := shared.GetCfg(r)
127-
128-	user, err := dbpool.FindUserForName(username)
129-	if err != nil {
130-		logger.Infof("blog not found: %s", username)
131-		http.Error(w, "blog not found", http.StatusNotFound)
132-		return
133-	}
134-
135-	tag := r.URL.Query().Get("tag")
136-	posts, err := getPostsForUser(r, user, tag, 1000)
137-	if err != nil {
138-		logger.Error(err)
139-		http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
140-		return
141-	}
142-
143-	curl := shared.CreateURLFromRequest(cfg, r)
144-
145-	ts, err := shared.RenderTemplate(cfg, []string{
146-		cfg.StaticPath("html/blog-default.partial.tmpl"),
147-		cfg.StaticPath("html/blog-aside.partial.tmpl"),
148-		cfg.StaticPath("html/blog.page.tmpl"),
149-		cfg.StaticPath("html/list.partial.tmpl"),
150-	})
151-
152-	if err != nil {
153-		logger.Error(err)
154-		http.Error(w, err.Error(), http.StatusInternalServerError)
155-		return
156-	}
157-
158-	headerTxt := &HeaderTxt{
159-		Title: GetBlogName(username),
160-		Bio:   "",
161-	}
162-	header, err := dbpool.FindPostWithFilename("_header.txt", user.ID, cfg.Space)
163-	if err == nil {
164-		parsedText := shared.ListParseText(header.Text)
165-		if parsedText.Title != "" {
166-			headerTxt.Title = parsedText.Title
167-		}
168-
169-		if parsedText.Description != "" {
170-			headerTxt.Bio = parsedText.Description
171-		}
172-
173-		if parsedText.Layout != "" {
174-			headerTxt.Layout = parsedText.Layout
175-		}
176-
177-		headerTxt.Nav = parsedText.Items
178-		if len(headerTxt.Nav) > 0 {
179-			headerTxt.HasItems = true
180-		}
181-	}
182-
183-	readmeTxt := &ReadmeTxt{}
184-	readme, err := dbpool.FindPostWithFilename("_readme.txt", user.ID, cfg.Space)
185-	if err == nil {
186-		parsedText := shared.ListParseText(readme.Text)
187-		readmeTxt.Items = parsedText.Items
188-		readmeTxt.ListType = parsedText.ListType
189-		if len(readmeTxt.Items) > 0 {
190-			readmeTxt.HasItems = true
191-		}
192-	}
193-
194-	postCollection := make([]PostItemData, 0, len(posts))
195-	for _, post := range posts {
196-		p := PostItemData{
197-			URL:            template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
198-			BlogURL:        template.URL(cfg.FullBlogURL(curl, post.Username)),
199-			Title:          shared.FilenameToTitle(post.Filename, post.Title),
200-			PublishAt:      post.PublishAt.Format("02 Jan, 2006"),
201-			PublishAtISO:   post.PublishAt.Format(time.RFC3339),
202-			UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
203-			UpdatedAtISO:   post.UpdatedAt.Format(time.RFC3339),
204-		}
205-		postCollection = append(postCollection, p)
206-	}
207-
208-	data := BlogPageData{
209-		Site:      *cfg.GetSiteData(),
210-		PageTitle: headerTxt.Title,
211-		URL:       template.URL(cfg.FullBlogURL(curl, username)),
212-		RSSURL:    template.URL(cfg.RssBlogURL(curl, username, tag)),
213-		Readme:    readmeTxt,
214-		Header:    headerTxt,
215-		Username:  username,
216-		Posts:     postCollection,
217-		HasFilter: tag != "",
218-	}
219-
220-	err = ts.Execute(w, data)
221-	if err != nil {
222-		logger.Error(err)
223-		http.Error(w, err.Error(), http.StatusInternalServerError)
224-	}
225-}
226-
227-func GetPostTitle(post *db.Post) string {
228-	if post.Description == "" {
229-		return post.Title
230-	}
231-
232-	return fmt.Sprintf("%s: %s", post.Title, post.Description)
233-}
234-
235-func GetBlogName(username string) string {
236-	return fmt.Sprintf("%s's lists", username)
237-}
238-
239-func postRawHandler(w http.ResponseWriter, r *http.Request) {
240-	username := shared.GetUsernameFromRequest(r)
241-	subdomain := shared.GetSubdomain(r)
242-	cfg := shared.GetCfg(r)
243-
244-	var slug string
245-	if !cfg.IsSubdomains() || subdomain == "" {
246-		slug, _ = url.PathUnescape(shared.GetField(r, 1))
247-	} else {
248-		slug, _ = url.PathUnescape(shared.GetField(r, 0))
249-	}
250-
251-	dbpool := shared.GetDB(r)
252-	logger := shared.GetLogger(r)
253-
254-	user, err := dbpool.FindUserForName(username)
255-	if err != nil {
256-		logger.Infof("blog not found: %s", username)
257-		http.Error(w, "blog not found", http.StatusNotFound)
258-		return
259-	}
260-
261-	post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
262-	if err != nil {
263-		logger.Infof("post not found")
264-		http.Error(w, "post not found", http.StatusNotFound)
265-		return
266-	}
267-
268-	w.Header().Add("Content-Type", "text/plain")
269-
270-	_, err = w.Write([]byte(post.Text))
271-	if err != nil {
272-		logger.Error(err)
273-		http.Error(w, "server error", 500)
274-	}
275-}
276-
277-func postHandler(w http.ResponseWriter, r *http.Request) {
278-	username := shared.GetUsernameFromRequest(r)
279-	subdomain := shared.GetSubdomain(r)
280-	cfg := shared.GetCfg(r)
281-
282-	var slug string
283-	if !cfg.IsSubdomains() || subdomain == "" {
284-		slug, _ = url.PathUnescape(shared.GetField(r, 1))
285-	} else {
286-		slug, _ = url.PathUnescape(shared.GetField(r, 0))
287-	}
288-
289-	dbpool := shared.GetDB(r)
290-	logger := shared.GetLogger(r)
291-
292-	user, err := dbpool.FindUserForName(username)
293-	if err != nil {
294-		logger.Infof("blog not found: %s", username)
295-		http.Error(w, "blog not found", http.StatusNotFound)
296-		return
297-	}
298-
299-	header, _ := dbpool.FindPostWithFilename("_header.txt", user.ID, cfg.Space)
300-	blogName := GetBlogName(username)
301-	if header != nil {
302-		headerParsed := shared.ListParseText(header.Text)
303-		if headerParsed.Title != "" {
304-			blogName = headerParsed.Title
305-		}
306-	}
307-
308-	var data PostPageData
309-	post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
310-	if err == nil {
311-		parsedText := shared.ListParseText(post.Text)
312-
313-		// we need the blog name from the readme unfortunately
314-		readme, err := dbpool.FindPostWithFilename("_readme.txt", user.ID, cfg.Space)
315-		if err == nil {
316-			readmeParsed := shared.ListParseText(readme.Text)
317-			if readmeParsed.Title != "" {
318-				blogName = readmeParsed.Title
319-			}
320-		}
321-
322-		// validate and fire off analytic event
323-		if isRequestTrackable(r) {
324-			_, err := dbpool.AddViewCount(post.ID)
325-			if err != nil {
326-				logger.Error(err)
327-			}
328-		}
329-
330-		data = PostPageData{
331-			Site:         *cfg.GetSiteData(),
332-			PageTitle:    GetPostTitle(post),
333-			URL:          template.URL(cfg.PostURL(post.Username, post.Slug)),
334-			BlogURL:      template.URL(cfg.BlogURL(username)),
335-			Description:  post.Description,
336-			ListType:     parsedText.ListType,
337-			Title:        shared.FilenameToTitle(post.Filename, post.Title),
338-			PublishAt:    post.PublishAt.Format("02 Jan, 2006"),
339-			PublishAtISO: post.PublishAt.Format(time.RFC3339),
340-			Username:     username,
341-			BlogName:     blogName,
342-			Items:        parsedText.Items,
343-			Tags:         parsedText.Tags,
344-		}
345-	} else {
346-		logger.Infof("post not found %s/%s", username, slug)
347-		data = PostPageData{
348-			Site:         *cfg.GetSiteData(),
349-			PageTitle:    "Post not found",
350-			Description:  "Post not found",
351-			Title:        "Post not found",
352-			ListType:     "none",
353-			BlogURL:      template.URL(cfg.BlogURL(username)),
354-			PublishAt:    time.Now().Format("02 Jan, 2006"),
355-			PublishAtISO: time.Now().Format(time.RFC3339),
356-			Username:     username,
357-			BlogName:     blogName,
358-			Items: []*shared.ListItem{
359-				{
360-					Value:  "oops!  we can't seem to find this post.",
361-					IsText: true,
362-				},
363-			},
364-		}
365-	}
366-
367-	ts, err := shared.RenderTemplate(cfg, []string{
368-		cfg.StaticPath("html/post.page.tmpl"),
369-		cfg.StaticPath("html/list.partial.tmpl"),
370-	})
371-
372-	if err != nil {
373-		http.Error(w, err.Error(), http.StatusInternalServerError)
374-	}
375-
376-	err = ts.Execute(w, data)
377-	if err != nil {
378-		logger.Error(err)
379-		http.Error(w, err.Error(), http.StatusInternalServerError)
380-	}
381-}
382-
383-func readHandler(w http.ResponseWriter, r *http.Request) {
384-	dbpool := shared.GetDB(r)
385-	logger := shared.GetLogger(r)
386-	cfg := shared.GetCfg(r)
387-
388-	page, _ := strconv.Atoi(r.URL.Query().Get("page"))
389-	tag := r.URL.Query().Get("tag")
390-	var pager *db.Paginate[*db.Post]
391-	var err error
392-	if tag == "" {
393-		pager, err = dbpool.FindAllUpdatedPosts(&db.Pager{Num: 30, Page: page}, cfg.Space)
394-	} else {
395-		pager, err = dbpool.FindPostsByTag(&db.Pager{Num: 30, Page: page}, tag, cfg.Space)
396-	}
397-
398-	if err != nil {
399-		logger.Error(err)
400-		http.Error(w, err.Error(), http.StatusInternalServerError)
401-		return
402-	}
403-
404-	ts, err := shared.RenderTemplate(cfg, []string{
405-		cfg.StaticPath("html/read.page.tmpl"),
406-	})
407-
408-	if err != nil {
409-		http.Error(w, err.Error(), http.StatusInternalServerError)
410-	}
411-
412-	nextPage := ""
413-	if page < pager.Total-1 {
414-		nextPage = fmt.Sprintf("/read?page=%d", page+1)
415-		if tag != "" {
416-			nextPage = fmt.Sprintf("%s&tag=%s", nextPage, tag)
417-		}
418-	}
419-
420-	prevPage := ""
421-	if page > 0 {
422-		prevPage = fmt.Sprintf("/read?page=%d", page-1)
423-		if tag != "" {
424-			prevPage = fmt.Sprintf("%s&tag=%s", prevPage, tag)
425-		}
426-	}
427-
428-	tags, err := dbpool.FindPopularTags(cfg.Space)
429-	if err != nil {
430-		logger.Error(err)
431-	}
432-
433-	data := ReadPageData{
434-		Site:      *cfg.GetSiteData(),
435-		NextPage:  nextPage,
436-		PrevPage:  prevPage,
437-		Tags:      tags,
438-		HasFilter: tag != "",
439-	}
440-	for _, post := range pager.Data {
441-		item := PostItemData{
442-			URL:            template.URL(cfg.PostURL(post.Username, post.Slug)),
443-			BlogURL:        template.URL(cfg.BlogURL(post.Username)),
444-			Title:          shared.FilenameToTitle(post.Filename, post.Title),
445-			Description:    post.Description,
446-			Username:       post.Username,
447-			PublishAt:      post.PublishAt.Format("02 Jan, 2006"),
448-			PublishAtISO:   post.PublishAt.Format(time.RFC3339),
449-			UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
450-			UpdatedAtISO:   post.UpdatedAt.Format(time.RFC3339),
451-		}
452-		data.Posts = append(data.Posts, item)
453-	}
454-
455-	err = ts.Execute(w, data)
456-	if err != nil {
457-		logger.Error(err)
458-		http.Error(w, err.Error(), http.StatusInternalServerError)
459-	}
460-}
461-
462-func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
463-	username := shared.GetUsernameFromRequest(r)
464-	dbpool := shared.GetDB(r)
465-	logger := shared.GetLogger(r)
466-	cfg := shared.GetCfg(r)
467-
468-	user, err := dbpool.FindUserForName(username)
469-	if err != nil {
470-		logger.Infof("rss feed not found: %s", username)
471-		http.Error(w, "rss feed not found", http.StatusNotFound)
472-		return
473-	}
474-
475-	tag := r.URL.Query().Get("tag")
476-	posts, err := getPostsForUser(r, user, tag, 10)
477-	if err != nil {
478-		logger.Error(err)
479-		http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
480-		return
481-	}
482-
483-	ts, err := template.New("rss.page.tmpl").Funcs(shared.FuncMap).ParseFiles(
484-		cfg.StaticPath("html/rss.page.tmpl"),
485-		cfg.StaticPath("html/list.partial.tmpl"),
486-	)
487-	if err != nil {
488-		logger.Error(err)
489-		http.Error(w, err.Error(), http.StatusInternalServerError)
490-		return
491-	}
492-
493-	headerTxt := &HeaderTxt{
494-		Title: GetBlogName(username),
495-	}
496-	header, err := dbpool.FindPostWithFilename("_header.txt", user.ID, cfg.Space)
497-	if err == nil {
498-		parsedText := shared.ListParseText(header.Text)
499-		if parsedText.Title != "" {
500-			headerTxt.Title = parsedText.Title
501-		}
502-
503-		if parsedText.Description != "" {
504-			headerTxt.Bio = parsedText.Description
505-		}
506-	}
507-
508-	feed := &feeds.Feed{
509-		Title:       headerTxt.Title,
510-		Link:        &feeds.Link{Href: cfg.BlogURL(username)},
511-		Description: headerTxt.Bio,
512-		Author:      &feeds.Author{Name: username},
513-		Created:     time.Now(),
514-	}
515-
516-	var feedItems []*feeds.Item
517-	for _, post := range posts {
518-		if slices.Contains(cfg.HiddenPosts, post.Filename) {
519-			continue
520-		}
521-		parsed := shared.ListParseText(post.Text)
522-		var tpl bytes.Buffer
523-		data := &PostPageData{
524-			ListType: parsed.ListType,
525-			Items:    parsed.Items,
526-		}
527-		if err := ts.Execute(&tpl, data); err != nil {
528-			logger.Error(err)
529-			continue
530-		}
531-
532-		item := &feeds.Item{
533-			Id:          cfg.PostURL(post.Username, post.Slug),
534-			Title:       shared.FilenameToTitle(post.Filename, post.Title),
535-			Link:        &feeds.Link{Href: cfg.PostURL(post.Username, post.Slug)},
536-			Content:     tpl.String(),
537-			Created:     *post.PublishAt,
538-			Updated:     *post.UpdatedAt,
539-			Description: post.Description,
540-		}
541-
542-		if post.Description != "" {
543-			item.Description = post.Description
544-		}
545-
546-		feedItems = append(feedItems, item)
547-	}
548-	feed.Items = feedItems
549-
550-	rss, err := feed.ToAtom()
551-	if err != nil {
552-		logger.Error(err)
553-		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
554-	}
555-
556-	w.Header().Add("Content-Type", "application/atom+xml")
557-	_, err = w.Write([]byte(rss))
558-	if err != nil {
559-		logger.Error(err)
560-	}
561-}
562-
563-func rssHandler(w http.ResponseWriter, r *http.Request) {
564-	dbpool := shared.GetDB(r)
565-	logger := shared.GetLogger(r)
566-	cfg := shared.GetCfg(r)
567-
568-	pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, cfg.Space)
569-	if err != nil {
570-		logger.Error(err)
571-		http.Error(w, err.Error(), http.StatusInternalServerError)
572-		return
573-	}
574-
575-	ts, err := template.New("rss.page.tmpl").Funcs(shared.FuncMap).ParseFiles(
576-		cfg.StaticPath("html/rss.page.tmpl"),
577-		cfg.StaticPath("html/list.partial.tmpl"),
578-	)
579-	if err != nil {
580-		logger.Error(err)
581-		http.Error(w, err.Error(), http.StatusInternalServerError)
582-		return
583-	}
584-
585-	feed := &feeds.Feed{
586-		Title:       fmt.Sprintf("%s discovery feed", cfg.Domain),
587-		Link:        &feeds.Link{Href: cfg.ReadURL()},
588-		Description: fmt.Sprintf("%s latest posts", cfg.Domain),
589-		Author:      &feeds.Author{Name: cfg.Domain},
590-		Created:     time.Now(),
591-	}
592-
593-	var feedItems []*feeds.Item
594-	for _, post := range pager.Data {
595-		parsed := shared.ListParseText(post.Text)
596-		var tpl bytes.Buffer
597-		data := &PostPageData{
598-			ListType: parsed.ListType,
599-			Items:    parsed.Items,
600-		}
601-		if err := ts.Execute(&tpl, data); err != nil {
602-			logger.Error(err)
603-			continue
604-		}
605-
606-		item := &feeds.Item{
607-			Id:          cfg.PostURL(post.Username, post.Slug),
608-			Title:       post.Title,
609-			Link:        &feeds.Link{Href: cfg.PostURL(post.Username, post.Slug)},
610-			Content:     tpl.String(),
611-			Created:     *post.PublishAt,
612-			Updated:     *post.UpdatedAt,
613-			Description: post.Description,
614-			Author:      &feeds.Author{Name: post.Username},
615-		}
616-
617-		if post.Description != "" {
618-			item.Description = post.Description
619-		}
620-
621-		feedItems = append(feedItems, item)
622-	}
623-	feed.Items = feedItems
624-
625-	rss, err := feed.ToAtom()
626-	if err != nil {
627-		logger.Error(err)
628-		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
629-	}
630-
631-	w.Header().Add("Content-Type", "application/atom+xml")
632-	_, err = w.Write([]byte(rss))
633-	if err != nil {
634-		logger.Error(err)
635-	}
636-}
637-
638-func createStaticRoutes() []shared.Route {
639-	return []shared.Route{
640-		shared.NewRoute("GET", "/main.css", shared.ServeFile("main.css", "text/css")),
641-		shared.NewRoute("GET", "/lists.css", shared.ServeFile("lists.css", "text/css")),
642-		shared.NewRoute("GET", "/card.png", shared.ServeFile("card.png", "image/png")),
643-		shared.NewRoute("GET", "/favicon-16x16.png", shared.ServeFile("favicon-16x16.png", "image/png")),
644-		shared.NewRoute("GET", "/favicon-32x32.png", shared.ServeFile("favicon-32x32.png", "image/png")),
645-		shared.NewRoute("GET", "/apple-touch-icon.png", shared.ServeFile("apple-touch-icon.png", "image/png")),
646-		shared.NewRoute("GET", "/favicon.ico", shared.ServeFile("favicon.ico", "image/x-icon")),
647-		shared.NewRoute("GET", "/robots.txt", shared.ServeFile("robots.txt", "text/plain")),
648-	}
649-}
650-
651-func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
652-	routes := []shared.Route{
653-		shared.NewRoute("GET", "/", readHandler),
654-		shared.NewRoute("GET", "/read", readHandler),
655-		shared.NewRoute("GET", "/check", shared.CheckHandler),
656-	}
657-
658-	routes = append(
659-		routes,
660-		staticRoutes...,
661-	)
662-
663-	routes = append(
664-		routes,
665-		shared.NewRoute("GET", "/rss", rssHandler),
666-		shared.NewRoute("GET", "/rss.xml", rssHandler),
667-		shared.NewRoute("GET", "/atom.xml", rssHandler),
668-		shared.NewRoute("GET", "/feed.xml", rssHandler),
669-
670-		shared.NewRoute("GET", "/([^/]+)", blogHandler),
671-		shared.NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
672-		shared.NewRoute("GET", "/([^/]+)/rss.xml", rssBlogHandler),
673-		shared.NewRoute("GET", "/([^/]+)/atom.xml", rssBlogHandler),
674-		shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
675-		shared.NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
676-		shared.NewRoute("GET", "/raw/([^/]+)/([^/]+)", postRawHandler),
677-	)
678-
679-	return routes
680-}
681-
682-func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
683-	routes := []shared.Route{
684-		shared.NewRoute("GET", "/", blogHandler),
685-		shared.NewRoute("GET", "/rss", rssBlogHandler),
686-		shared.NewRoute("GET", "/rss.xml", rssBlogHandler),
687-		shared.NewRoute("GET", "/atom.xml", rssBlogHandler),
688-		shared.NewRoute("GET", "/feed.xml", rssBlogHandler),
689-	}
690-
691-	routes = append(
692-		routes,
693-		staticRoutes...,
694-	)
695-
696-	routes = append(
697-		routes,
698-		shared.NewRoute("GET", "/([^/]+)", postHandler),
699-		shared.NewRoute("GET", "/raw/([^/]+)", postRawHandler),
700-	)
701-
702-	return routes
703-}
704-
705-func StartApiServer() {
706-	cfg := NewConfigSite()
707-	db := postgres.NewDB(cfg.DbURL, cfg.Logger)
708-	defer db.Close()
709-	logger := cfg.Logger
710-
711-	var st storage.ObjectStorage
712-	var err error
713-	if cfg.MinioURL == "" {
714-		st, err = storage.NewStorageFS(cfg.StorageDir)
715-	} else {
716-		st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
717-	}
718-
719-	cache := gocache.New(2*time.Minute, 5*time.Minute)
720-
721-	if err != nil {
722-		logger.Fatal(err)
723-	}
724-
725-	staticRoutes := createStaticRoutes()
726-
727-	if cfg.Debug {
728-		staticRoutes = shared.CreatePProfRoutes(staticRoutes)
729-	}
730-
731-	mainRoutes := createMainRoutes(staticRoutes)
732-	subdomainRoutes := createSubdomainRoutes(staticRoutes)
733-
734-	handler := shared.CreateServe(mainRoutes, subdomainRoutes, cfg, db, st, logger, cache)
735-	router := http.HandlerFunc(handler)
736-
737-	portStr := fmt.Sprintf(":%s", cfg.Port)
738-	logger.Infof("Starting server on port %s", cfg.Port)
739-	logger.Infof("Subdomains enabled: %t", cfg.SubdomainsEnabled)
740-	logger.Infof("Domain: %s", cfg.Domain)
741-	logger.Infof("Email: %s", cfg.Email)
742-
743-	logger.Fatal(http.ListenAndServe(portStr, router))
744-}
D lists/config.go
+0, -51
 1@@ -1,51 +0,0 @@
 2-package lists
 3-
 4-import (
 5-	"github.com/picosh/pico/shared"
 6-	"github.com/picosh/pico/wish/cms/config"
 7-)
 8-
 9-func NewConfigSite() *shared.ConfigSite {
10-	debug := shared.GetEnv("LISTS_DEBUG", "0")
11-	domain := shared.GetEnv("LISTS_DOMAIN", "lists.sh")
12-	email := shared.GetEnv("LISTS_EMAIL", "support@lists.sh")
13-	subdomains := shared.GetEnv("LISTS_SUBDOMAINS", "0")
14-	customdomains := shared.GetEnv("LISTS_CUSTOMDOMAINS", "0")
15-	port := shared.GetEnv("LISTS_WEB_PORT", "3000")
16-	protocol := shared.GetEnv("LISTS_PROTOCOL", "https")
17-	allowRegister := shared.GetEnv("LISTS_ALLOW_REGISTER", "1")
18-	storageDir := shared.GetEnv("IMGS_STORAGE_DIR", ".storage")
19-	minioURL := shared.GetEnv("MINIO_URL", "")
20-	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
21-	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
22-	dbURL := shared.GetEnv("DATABASE_URL", "")
23-	useImgProxy := shared.GetEnv("USE_IMGPROXY", "1")
24-
25-	intro := "To get started, enter a username.\n"
26-	intro += "To learn next steps go to our docs at https://pico.sh/lists\n"
27-
28-	return &shared.ConfigSite{
29-		Debug:                debug == "1",
30-		SubdomainsEnabled:    subdomains == "1",
31-		CustomdomainsEnabled: customdomains == "1",
32-		UseImgProxy:          useImgProxy == "1",
33-		ConfigCms: config.ConfigCms{
34-			Domain:        domain,
35-			Email:         email,
36-			Port:          port,
37-			Protocol:      protocol,
38-			DbURL:         dbURL,
39-			StorageDir:    storageDir,
40-			MinioURL:      minioURL,
41-			MinioUser:     minioUser,
42-			MinioPass:     minioPass,
43-			Description:   "A microblog for your lists",
44-			IntroText:     intro,
45-			Space:         "lists",
46-			AllowedExt:    []string{".txt"},
47-			HiddenPosts:   []string{"_header.txt", "_readme.txt"},
48-			Logger:        shared.CreateLogger(debug == "1"),
49-			AllowRegister: allowRegister == "1",
50-		},
51-	}
52-}
D lists/html/base.layout.tmpl
+0, -25
 1@@ -1,25 +0,0 @@
 2-{{define "base"}}
 3-<!doctype html>
 4-<html lang="en">
 5-    <head>
 6-        <meta charset='utf-8'>
 7-        <meta name="viewport" content="width=device-width, initial-scale=1" />
 8-        <title>{{template "title" .}}</title>
 9-
10-        <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
11-
12-        <meta name="keywords" content="blog, blogging, write, writing, lists" />
13-        {{template "meta" .}}
14-
15-        <link rel="stylesheet" href="https://pico.sh/smol.css" />
16-        <link rel="stylesheet" href="/main.css" />
17-    </head>
18-    <body {{template "attrs" .}}>
19-      <div class="box mb-2 text-center">
20-        This service will shut down on <strong>2024-07-20</strong>.
21-        <a href="https://blog.pico.sh/lists-shutdown-notice">Read our post about it.</a>
22-      </div>
23-      {{template "body" .}}
24-    </body>
25-</html>
26-{{end}}
D lists/html/blog-aside.partial.tmpl
+0, -67
 1@@ -1,67 +0,0 @@
 2-{{define "blog-aside"}}
 3-<main class="flex">
 4-    <section class="flex-1 mr">
 5-        <div>
 6-            <h1 class="text-2xl font-bold">{{.Header.Title}}</h1>
 7-            {{if .Header.Bio}}<span>{{.Header.Bio}}</span>{{end}}
 8-        </div>
 9-
10-        <div id="readme">
11-            {{if .Readme.HasItems}}
12-            <section>
13-                <article>
14-                    {{template "list" .Readme}}
15-                </article>
16-                <hr />
17-            </section>
18-            {{end}}
19-
20-            <ul>
21-                {{range .Header.Nav}}
22-                    {{if .IsURL}}
23-                    <li><a href="{{.URL}}" class="text-lg">{{.Value}}</a></li>
24-                    {{end}}
25-                {{end}}
26-                <li><a href="{{.RSSURL}}" class="text-lg">rss</a></li>
27-            </ul>
28-            <hr />
29-        </div>
30-
31-        {{if .HasFilter}}
32-            <a href={{.URL}}>clear filters</a>
33-        {{end}}
34-
35-        <div class="posts">
36-        {{range .Posts}}
37-            <article class="my-2">
38-                <div class="flex items-center">
39-                    <time datetime="{{.PublishAtISO}}" class="text-sm post-date">{{.PublishAt}}</time>
40-                    <span class="text-md flex-1"><a href="{{.URL}}">{{.Title}}</a></span>
41-                </div>
42-            </article>
43-        {{end}}
44-        </div>
45-    </section>
46-
47-    <aside>
48-        {{if .Readme.HasItems}}
49-        <section>
50-            <article>
51-                {{template "list" .Readme}}
52-            </article>
53-        </section>
54-        {{end}}
55-
56-        <nav>
57-            <ul>
58-                {{range .Header.Nav}}
59-                    {{if .IsURL}}
60-                    <li><a href="{{.URL}}" class="text-md">{{.Value}}</a></li>
61-                    {{end}}
62-                {{end}}
63-                <li><a href="{{.RSSURL}}" class="text-md">rss</a></li>
64-            </ul>
65-        </nav>
66-    </aside>
67-</main>
68-{{end}}
D lists/html/blog-default.partial.tmpl
+0, -39
 1@@ -1,39 +0,0 @@
 2-{{define "blog-default"}}
 3-<header class="text-center">
 4-    <h1 class="text-2xl font-bold">{{.Header.Title}}</h1>
 5-    {{if .Header.Bio}}<p class="text-lg">{{.Header.Bio}}</p>{{end}}
 6-    <nav>
 7-        {{range .Header.Nav}}
 8-            {{if .IsURL}}
 9-            <a href="{{.URL}}" class="text-md">{{.Value}}</a> |
10-            {{end}}
11-        {{end}}
12-        <a href="{{.RSSURL}}" class="text-md">rss</a>
13-    </nav>
14-    <hr />
15-</header>
16-<main>
17-    {{if .Readme.HasItems}}
18-    <section>
19-        <article>
20-            {{template "list" .Readme}}
21-        </article>
22-        <hr />
23-    </section>
24-    {{end}}
25-
26-    <section class="posts">
27-        {{if .HasFilter}}
28-            <a href="{{.URL}}">clear filters</a>
29-        {{end}}
30-        {{range .Posts}}
31-        <article class="my-2">
32-            <div class="flex items-center">
33-                <time datetime="{{.PublishAtISO}}" class="text-sm post-date">{{.PublishAt}}</time>
34-                <span class="text-md flex-1"><a href="{{.URL}}">{{.Title}}</a></span>
35-            </div>
36-        </article>
37-        {{end}}
38-    </section>
39-</main>
40-{{end}}
D lists/html/blog.page.tmpl
+0, -39
 1@@ -1,39 +0,0 @@
 2-{{template "base" .}}
 3-
 4-{{define "title"}}{{.PageTitle}}{{end}}
 5-
 6-{{define "meta"}}
 7-<meta name="description" content="{{if .Header.Bio}}{{.Header.Bio}}{{else}}{{.Header.Title}}{{end}}" />
 8-
 9-<meta property="og:type" content="website">
10-<meta property="og:site_name" content="{{.Site.Domain}}">
11-<meta property="og:url" content="{{.URL}}">
12-<meta property="og:title" content="{{.Header.Title}}">
13-{{if .Header.Bio}}<meta property="og:description" content="{{.Header.Bio}}">{{end}}
14-<meta property="og:image:width" content="300" />
15-<meta property="og:image:height" content="300" />
16-<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
17-<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
18-
19-<meta property="twitter:card" content="summary">
20-<meta property="twitter:url" content="{{.URL}}">
21-<meta property="twitter:title" content="{{.Header.Title}}">
22-{{if .Header.Bio}}<meta property="twitter:description" content="{{.Header.Bio}}">{{end}}
23-<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
24-<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
25-
26-<link rel="alternate" href="{{.RSSURL}}" type="application/rss+xml" title="RSS feed for {{.Header.Title}}" />
27-{{end}}
28-
29-{{define "attrs"}}id="blog" class="layout-{{.Header.Layout}}"{{end}}
30-
31-{{define "body"}}
32-
33-{{if eq .Header.Layout "aside"}}
34-    {{template "blog-aside" .}}
35-{{else}}
36-    {{template "blog-default" .}}
37-{{end}}
38-
39-{{template "footer" .}}
40-{{end}}
D lists/html/footer.partial.tmpl
+0, -6
1@@ -1,6 +0,0 @@
2-{{define "footer"}}
3-<footer>
4-    <hr />
5-    published with <a href={{.Site.HomeURL}}>{{.Site.Domain}}</a>
6-</footer>
7-{{end}}
D lists/html/list.partial.tmpl
+0, -51
 1@@ -1,51 +0,0 @@
 2-{{define "list"}}
 3-{{$indent := 0}}
 4-{{$mod := 0}}
 5-<ul style="list-style-type: {{.ListType}};">
 6-    {{range .Items}}
 7-        {{if lt $indent .Indent}}
 8-        <ul style="list-style-type: {{$.ListType}};">
 9-        {{else if gt $indent .Indent}}
10-
11-        {{$mod = minus $indent .Indent}}
12-        {{range $y := intRange 1 $mod}}
13-        </li></ul>
14-        {{end}}
15-
16-        {{else}}
17-        </li>
18-        {{end}}
19-        {{$indent = .Indent}}
20-
21-        {{if .IsText}}
22-            {{if .Value}}
23-            <li>{{.Value}}
24-            {{end}}
25-        {{end}}
26-
27-        {{if .IsURL}}
28-        <li><a href="{{.URL}}">{{.Value}}</a>
29-        {{end}}
30-
31-        {{if .IsImg}}
32-        <li><img src="{{.URL}}" alt="{{.Value}}" />
33-        {{end}}
34-
35-        {{if .IsBlock}}
36-        <li><blockquote>{{.Value}}</blockquote>
37-        {{end}}
38-
39-        {{if .IsHeaderOne}}
40-        </ul><h2 class="text-xl font-bold">{{.Value}}</h2><ul style="list-style-type: {{$.ListType}};">
41-        {{end}}
42-
43-        {{if .IsHeaderTwo}}
44-        </ul><h3 class="text-lg font-bold">{{.Value}}</h3><ul style="list-style-type: {{$.ListType}};">
45-        {{end}}
46-
47-        {{if .IsPre}}
48-        <li><pre>{{.Value}}</pre>
49-        {{end}}
50-    {{end}}
51-</ul>
52-{{end}}
D lists/html/marketing-footer.partial.tmpl
+0, -9
 1@@ -1,9 +0,0 @@
 2-{{define "marketing-footer"}}
 3-<footer>
 4-    <hr />
 5-    <p class="font-italic">Built and maintained by <a href="https://pico.sh">pico.sh</a>.</p>
 6-    <div>
 7-        <a href="/rss">rss</a>
 8-    </div>
 9-</footer>
10-{{end}}
D lists/html/post.page.tmpl
+0, -48
 1@@ -1,48 +0,0 @@
 2-{{template "base" .}}
 3-
 4-{{define "title"}}{{.PageTitle}}{{end}}
 5-
 6-{{define "meta"}}
 7-<meta name="description" content="{{.Description}}" />
 8-
 9-<meta property="og:type" content="website">
10-<meta property="og:site_name" content="{{.Site.Domain}}">
11-<meta property="og:url" content="{{.URL}}">
12-<meta property="og:title" content="{{.Title}}">
13-{{if .Description}}<meta property="og:description" content="{{.Description}}">{{end}}
14-<meta property="og:image:width" content="300" />
15-<meta property="og:image:height" content="300" />
16-<meta itemprop="image" content="https://{{.Site.Domain}}/card.png" />
17-<meta property="og:image" content="https://{{.Site.Domain}}/card.png" />
18-
19-<meta property="twitter:card" content="summary">
20-<meta property="twitter:url" content="{{.URL}}">
21-<meta property="twitter:title" content="{{.Title}}">
22-{{if .Description}}<meta property="twitter:description" content="{{.Description}}">{{end}}
23-<meta name="twitter:image" content="https://{{.Site.Domain}}/card.png" />
24-<meta name="twitter:image:src" content="https://{{.Site.Domain}}/card.png" />
25-{{end}}
26-
27-{{define "attrs"}}{{end}}
28-
29-{{define "body"}}
30-<header>
31-    <h1 class="text-2xl font-bold">{{.Title}}</h1>
32-    <p class="font-bold m-0">
33-        <time datetime="{{.PublishAtISO}}">{{.PublishAt}}</time>
34-        <span> on </span>
35-        <a href="{{.BlogURL}}">{{.BlogName}}</a></p>
36-    {{if .Description}}<div class="my font-italic">{{.Description}}</div>{{end}}
37-    <div class="tags">
38-    {{range .Tags}}
39-        <a class="tag" href="{{$.BlogURL}}?tag={{.}}">#{{.}}</a>
40-    {{end}}
41-    </div>
42-</header>
43-<main>
44-    <article>
45-        {{template "list" .}}
46-    </article>
47-</main>
48-{{template "footer" .}}
49-{{end}}
D lists/html/read.page.tmpl
+0, -51
 1@@ -1,51 +0,0 @@
 2-{{template "base" .}}
 3-
 4-{{define "title"}}discover lists -- {{.Site.Domain}}{{end}}
 5-
 6-{{define "meta"}}
 7-<meta name="description" content="discover interesting lists" />
 8-{{end}}
 9-
10-{{define "attrs"}}{{end}}
11-
12-{{define "body"}}
13-<header class="text-center">
14-    <h1 class="text-2xl font-bold">lists.sh</h1>
15-    <p class="text-lg">A microblog for lists</p>
16-    <div>
17-      <a href="https://pico.sh/getting-started" class="btn-link mt inline-block">GET STARTED</a>
18-    </div>
19-    <hr />
20-</header>
21-<main>
22-    <div class="flex items-center">
23-        <div class="font-italic text-sm post-date">popular tags</div>
24-        <div class="flex-1">
25-        {{range .Tags}}
26-        <span class="text-md"><a href="/read?tag={{.}}">#{{.}}</a></span>
27-        {{end}}
28-        </div>
29-    </div>
30-    {{if .HasFilter}}<a href="/read">clear filter</a>{{end}}
31-
32-    <div class="mb">
33-        {{if .PrevPage}}<a href="{{.PrevPage}}">prev</a>{{else}}<span class="text-grey">prev</span>{{end}}
34-        {{if .NextPage}}<a href="{{.NextPage}}">next</a>{{else}}<span class="text-grey">next</span>{{end}}
35-    </div>
36-
37-    {{range .Posts}}
38-    <article class="my-2">
39-        <div class="flex items-center">
40-            <time datetime="{{.UpdatedAtISO}}" class="text-sm post-date">{{.UpdatedTimeAgo}}</time>
41-            <div class="flex-1">
42-                <span class="text-md"><a href="{{.URL}}">{{.Title}}</a></span>
43-                <address class="text-sm inline">
44-                    <a href="{{.BlogURL}}" class="link-grey">({{.Username}})</a>
45-                </address>
46-            </div>
47-        </div>
48-    </article>
49-    {{end}}
50-</main>
51-{{template "marketing-footer" .}}
52-{{end}}
D lists/html/rss.page.tmpl
+0, -1
1@@ -1 +0,0 @@
2-{{template "list" .}}
D lists/public/apple-touch-icon.png
+0, -0
D lists/public/card.png
+0, -0
D lists/public/favicon-16x16.png
+0, -0
D lists/public/favicon.ico
+0, -0
D lists/public/main.css
+0, -37
 1@@ -1,37 +0,0 @@
 2-body {
 3-  max-width: 720px;
 4-}
 5-
 6-.post-date {
 7-  width: 130px;
 8-}
 9-
10-.layout-aside {
11-  max-width: 50rem;
12-}
13-
14-.layout-aside aside {
15-  width: 200px;
16-}
17-
18-.layout-aside img {
19-  border-radius: 5px;
20-}
21-
22-#readme {
23-  display: none;
24-}
25-
26-@media only screen and (max-width: 600px) {
27-  .layout-aside main {
28-    flex-direction: column;
29-  }
30-
31-  aside {
32-    display: none;
33-  }
34-
35-  #readme {
36-    display: block;
37-  }
38-}
D lists/public/robots.txt
+0, -2
1@@ -1,2 +0,0 @@
2-User-agent: *
3-Allow: /
D lists/scp_hooks.go
+0, -61
 1@@ -1,61 +0,0 @@
 2-package lists
 3-
 4-import (
 5-	"fmt"
 6-	"strings"
 7-
 8-	"slices"
 9-
10-	"github.com/charmbracelet/ssh"
11-	"github.com/picosh/pico/db"
12-	"github.com/picosh/pico/filehandlers"
13-	"github.com/picosh/pico/shared"
14-)
15-
16-type ListHooks struct {
17-	Cfg *shared.ConfigSite
18-	Db  db.DB
19-}
20-
21-func (p *ListHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
22-	if !shared.IsTextFile(string(data.Text)) {
23-		err := fmt.Errorf(
24-			"WARNING: (%s) invalid file must be plain text (utf-8), skipping",
25-			data.Filename,
26-		)
27-		return false, err
28-	}
29-
30-	if !shared.IsExtAllowed(data.Filename, p.Cfg.AllowedExt) {
31-		extStr := strings.Join(p.Cfg.AllowedExt, ",")
32-		err := fmt.Errorf(
33-			"WARNING: (%s) invalid file, format must be (%s), skipping",
34-			data.Filename,
35-			extStr,
36-		)
37-		return false, err
38-	}
39-
40-	return true, nil
41-}
42-
43-func (p *ListHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
44-	parsedText := shared.ListParseText(string(data.Text))
45-
46-	if parsedText.Title == "" {
47-		data.Title = shared.ToUpper(data.Slug)
48-	} else {
49-		data.Title = parsedText.Title
50-	}
51-
52-	data.Description = parsedText.Description
53-	data.Tags = parsedText.Tags
54-
55-	if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
56-		data.PublishAt = parsedText.PublishAt
57-	}
58-
59-	data.Hidden = slices.Contains(p.Cfg.HiddenPosts, data.Filename)
60-
61-	return nil
62-}
D lists/ssh.go
+0, -122
  1@@ -1,122 +0,0 @@
  2-package lists
  3-
  4-import (
  5-	"context"
  6-	"fmt"
  7-	"os"
  8-	"os/signal"
  9-	"syscall"
 10-	"time"
 11-
 12-	"github.com/charmbracelet/promwish"
 13-	"github.com/charmbracelet/ssh"
 14-	"github.com/charmbracelet/wish"
 15-	bm "github.com/charmbracelet/wish/bubbletea"
 16-	lm "github.com/charmbracelet/wish/logging"
 17-	"github.com/picosh/pico/db/postgres"
 18-	"github.com/picosh/pico/filehandlers"
 19-	"github.com/picosh/pico/shared"
 20-	"github.com/picosh/pico/shared/storage"
 21-	"github.com/picosh/pico/wish/cms"
 22-	"github.com/picosh/send/list"
 23-	"github.com/picosh/send/pipe"
 24-	"github.com/picosh/send/proxy"
 25-	"github.com/picosh/send/send/auth"
 26-	wishrsync "github.com/picosh/send/send/rsync"
 27-	"github.com/picosh/send/send/scp"
 28-	"github.com/picosh/send/send/sftp"
 29-)
 30-
 31-type SSHServer struct{}
 32-
 33-func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
 34-	return true
 35-}
 36-
 37-func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
 38-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 39-		return []wish.Middleware{
 40-			pipe.Middleware(handler, ".txt"),
 41-			list.Middleware(handler),
 42-			scp.Middleware(handler),
 43-			wishrsync.Middleware(handler),
 44-			auth.Middleware(handler),
 45-			bm.Middleware(cms.Middleware(&handler.Cfg.ConfigCms, handler.Cfg)),
 46-			lm.Middleware(),
 47-		}
 48-	}
 49-}
 50-
 51-func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
 52-	return func(server *ssh.Server) error {
 53-		err := sftp.SSHOption(handler)(server)
 54-		if err != nil {
 55-			return err
 56-		}
 57-
 58-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
 59-	}
 60-}
 61-
 62-func StartSshServer() {
 63-	host := shared.GetEnv("LISTS_HOST", "0.0.0.0")
 64-	port := shared.GetEnv("LISTS_SSH_PORT", "2222")
 65-	promPort := shared.GetEnv("LISTS_PROM_PORT", "9222")
 66-	cfg := NewConfigSite()
 67-	logger := cfg.Logger
 68-	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 69-	defer dbh.Close()
 70-
 71-	hooks := &ListHooks{
 72-		Cfg: cfg,
 73-		Db:  dbh,
 74-	}
 75-
 76-	var st storage.ObjectStorage
 77-	var err error
 78-	if cfg.MinioURL == "" {
 79-		st, err = storage.NewStorageFS(cfg.StorageDir)
 80-	} else {
 81-		st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
 82-	}
 83-
 84-	if err != nil {
 85-		logger.Fatal(err)
 86-	}
 87-
 88-	fileMap := map[string]filehandlers.ReadWriteHandler{
 89-		"fallback": filehandlers.NewScpPostHandler(dbh, cfg, hooks, st),
 90-	}
 91-	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 92-
 93-	sshServer := &SSHServer{}
 94-	s, err := wish.NewServer(
 95-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 96-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 97-		wish.WithPublicKeyAuth(sshServer.authHandler),
 98-		withProxy(
 99-			handler,
100-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "lists-ssh"),
101-		),
102-	)
103-	if err != nil {
104-		logger.Fatal(err)
105-	}
106-
107-	done := make(chan os.Signal, 1)
108-	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
109-	logger.Infof("Starting SSH server on %s:%s", host, port)
110-	go func() {
111-		if err = s.ListenAndServe(); err != nil {
112-			logger.Fatal(err)
113-		}
114-	}()
115-
116-	<-done
117-	logger.Info("Stopping SSH server")
118-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
119-	defer func() { cancel() }()
120-	if err := s.Shutdown(ctx); err != nil {
121-		logger.Fatal(err)
122-	}
123-}
M pastes/api.go
+22, -19
  1@@ -83,7 +83,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
  2 
  3 	user, err := dbpool.FindUserForName(username)
  4 	if err != nil {
  5-		logger.Infof("blog not found: %s", username)
  6+		logger.Info("blog not found", "user", username)
  7 		http.Error(w, "blog not found", http.StatusNotFound)
  8 		return
  9 	}
 10@@ -91,7 +91,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 11 	posts := pager.Data
 12 
 13 	if err != nil {
 14-		logger.Error(err)
 15+		logger.Error(err.Error())
 16 		http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
 17 		return
 18 	}
 19@@ -101,7 +101,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 20 	})
 21 
 22 	if err != nil {
 23-		logger.Error(err)
 24+		logger.Error(err.Error())
 25 		http.Error(w, err.Error(), http.StatusInternalServerError)
 26 		return
 27 	}
 28@@ -138,7 +138,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 29 
 30 	err = ts.Execute(w, data)
 31 	if err != nil {
 32-		logger.Error(err)
 33+		logger.Error(err.Error())
 34 		http.Error(w, err.Error(), http.StatusInternalServerError)
 35 	}
 36 }
 37@@ -172,7 +172,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 38 
 39 	user, err := dbpool.FindUserForName(username)
 40 	if err != nil {
 41-		logger.Infof("blog not found: %s", username)
 42+		logger.Info("blog not found", "user", username)
 43 		http.Error(w, "blog not found", http.StatusNotFound)
 44 		return
 45 	}
 46@@ -184,7 +184,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 47 	if err == nil {
 48 		parsedText, err := ParseText(post.Filename, post.Text)
 49 		if err != nil {
 50-			logger.Error(err)
 51+			logger.Error(err.Error())
 52 		}
 53 		expiresAt := "never"
 54 		if post.ExpiresAt != nil {
 55@@ -207,7 +207,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 56 			ExpiresAt:    expiresAt,
 57 		}
 58 	} else {
 59-		logger.Infof("post not found %s/%s", username, slug)
 60+		logger.Info("post not found", "user", username, "slug", slug)
 61 		data = PostPageData{
 62 			Site:         *cfg.GetSiteData(),
 63 			PageTitle:    "Paste not found",
 64@@ -233,7 +233,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 65 
 66 	err = ts.Execute(w, data)
 67 	if err != nil {
 68-		logger.Error(err)
 69+		logger.Error(err.Error())
 70 		http.Error(w, err.Error(), http.StatusInternalServerError)
 71 	}
 72 }
 73@@ -255,14 +255,14 @@ func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
 74 
 75 	user, err := dbpool.FindUserForName(username)
 76 	if err != nil {
 77-		logger.Infof("blog not found: %s", username)
 78+		logger.Info("blog not found", "user", username)
 79 		http.Error(w, "blog not found", http.StatusNotFound)
 80 		return
 81 	}
 82 
 83 	post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
 84 	if err != nil {
 85-		logger.Infof("post not found %s/%s", username, slug)
 86+		logger.Info("post not found", "user", username, "slug", slug)
 87 		http.Error(w, "post not found", http.StatusNotFound)
 88 		return
 89 	}
 90@@ -270,7 +270,7 @@ func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
 91 	w.Header().Set("Content-Type", "text/plain")
 92 	_, err = w.Write([]byte(post.Text))
 93 	if err != nil {
 94-		logger.Error(err)
 95+		logger.Error(err.Error())
 96 	}
 97 }
 98 
 99@@ -281,14 +281,14 @@ func serveFile(file string, contentType string) http.HandlerFunc {
100 
101 		contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
102 		if err != nil {
103-			logger.Error(err)
104+			logger.Error(err.Error())
105 			http.Error(w, "file not found", 404)
106 		}
107 		w.Header().Add("Content-Type", contentType)
108 
109 		_, err = w.Write(contents)
110 		if err != nil {
111-			logger.Error(err)
112+			logger.Error(err.Error())
113 			http.Error(w, "server error", 500)
114 		}
115 	}
116@@ -367,7 +367,8 @@ func StartApiServer() {
117 	cache := gocache.New(2*time.Minute, 5*time.Minute)
118 
119 	if err != nil {
120-		logger.Fatal(err)
121+		logger.Error(err.Error())
122+		return
123 	}
124 
125 	go CronDeleteExpiredPosts(cfg, db)
126@@ -385,10 +386,12 @@ func StartApiServer() {
127 	router := http.HandlerFunc(handler)
128 
129 	portStr := fmt.Sprintf(":%s", cfg.Port)
130-	logger.Infof("Starting server on port %s", cfg.Port)
131-	logger.Infof("Subdomains enabled: %t", cfg.SubdomainsEnabled)
132-	logger.Infof("Domain: %s", cfg.Domain)
133-	logger.Infof("Email: %s", cfg.Email)
134+	logger.Info(
135+		"Starting server on port",
136+		"port", cfg.Port,
137+		"domain", cfg.Domain,
138+		"email", cfg.Email,
139+	)
140 
141-	logger.Fatal(http.ListenAndServe(portStr, router))
142+	logger.Error(http.ListenAndServe(portStr, router).Error())
143 }
M pastes/cron.go
+3, -3
 1@@ -8,7 +8,7 @@ import (
 2 )
 3 
 4 func deleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) error {
 5-	cfg.Logger.Infof("checking for expired posts")
 6+	cfg.Logger.Info("checking for expired posts")
 7 	posts, err := dbpool.FindExpiredPosts(cfg.Space)
 8 	if err != nil {
 9 		return err
10@@ -19,7 +19,7 @@ func deleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) error {
11 		postIds = append(postIds, post.ID)
12 	}
13 
14-	cfg.Logger.Infof("deleting (%d) expired posts", len(postIds))
15+	cfg.Logger.Info("deleting expired posts", "len", len(postIds))
16 	err = dbpool.RemovePosts(postIds)
17 	if err != nil {
18 		return err
19@@ -32,7 +32,7 @@ func CronDeleteExpiredPosts(cfg *shared.ConfigSite, dbpool db.DB) {
20 	for {
21 		err := deleteExpiredPosts(cfg, dbpool)
22 		if err != nil {
23-			cfg.Logger.Error(err)
24+			cfg.Logger.Error(err.Error())
25 		}
26 		time.Sleep(1 * time.Minute)
27 	}
M pastes/ssh.go
+7, -5
 1@@ -80,7 +80,8 @@ func StartSshServer() {
 2 	}
 3 
 4 	if err != nil {
 5-		logger.Fatal(err)
 6+		logger.Error(err.Error())
 7+		return
 8 	}
 9 
10 	fileMap := map[string]filehandlers.ReadWriteHandler{
11@@ -99,15 +100,16 @@ func StartSshServer() {
12 		),
13 	)
14 	if err != nil {
15-		logger.Fatal(err)
16+		logger.Error(err.Error())
17+		return
18 	}
19 
20 	done := make(chan os.Signal, 1)
21 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
22-	logger.Infof("Starting SSH server on %s:%s", host, port)
23+	logger.Info("Starting SSH server", "host", host, "port", port)
24 	go func() {
25 		if err = s.ListenAndServe(); err != nil {
26-			logger.Fatal(err)
27+			logger.Error(err.Error())
28 		}
29 	}()
30 
31@@ -116,6 +118,6 @@ func StartSshServer() {
32 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
33 	defer func() { cancel() }()
34 	if err := s.Shutdown(ctx); err != nil {
35-		logger.Fatal(err)
36+		logger.Error(err.Error())
37 	}
38 }
M pgs/api.go
+33, -27
  1@@ -4,6 +4,7 @@ import (
  2 	"fmt"
  3 	"html/template"
  4 	"io"
  5+	"log/slog"
  6 	"net/http"
  7 	"net/url"
  8 	"path/filepath"
  9@@ -20,7 +21,6 @@ import (
 10 	"github.com/picosh/pico/shared"
 11 	"github.com/picosh/pico/shared/storage"
 12 	"github.com/picosh/send/send/utils"
 13-	"go.uber.org/zap"
 14 )
 15 
 16 type AssetHandler struct {
 17@@ -31,7 +31,7 @@ type AssetHandler struct {
 18 	Cfg            *shared.ConfigSite
 19 	Dbpool         db.DB
 20 	Storage        storage.ObjectStorage
 21-	Logger         *zap.SugaredLogger
 22+	Logger         *slog.Logger
 23 	Cache          *gocache.Cache
 24 	UserID         string
 25 	Bucket         storage.Bucket
 26@@ -48,24 +48,28 @@ func checkHandler(w http.ResponseWriter, r *http.Request) {
 27 		appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
 28 
 29 		if !strings.Contains(hostDomain, appDomain) {
 30-			subdomain := shared.GetCustomDomain(logger, hostDomain, cfg.Space)
 31+			subdomain := shared.GetCustomDomain(hostDomain, cfg.Space)
 32 			props, err := getProjectFromSubdomain(subdomain)
 33 			if err != nil {
 34-				logger.Error(err)
 35+				logger.Error(err.Error())
 36 				w.WriteHeader(http.StatusNotFound)
 37 				return
 38 			}
 39 
 40 			u, err := dbpool.FindUserForName(props.Username)
 41 			if err != nil {
 42-				logger.Error(err)
 43+				logger.Error(err.Error())
 44 				w.WriteHeader(http.StatusNotFound)
 45 				return
 46 			}
 47 
 48+			logger = logger.With(
 49+				"user", u.Name,
 50+				"project", props.ProjectName,
 51+			)
 52 			p, err := dbpool.FindProjectByName(u.ID, props.ProjectName)
 53 			if err != nil {
 54-				logger.Error(err)
 55+				logger.Error(err.Error())
 56 				w.WriteHeader(http.StatusNotFound)
 57 				return
 58 			}
 59@@ -91,7 +95,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
 60 
 61 	pager, err := dbpool.FindAllProjects(&db.Pager{Num: 50, Page: 0})
 62 	if err != nil {
 63-		logger.Error(err)
 64+		logger.Error(err.Error())
 65 		http.Error(w, err.Error(), http.StatusInternalServerError)
 66 		return
 67 	}
 68@@ -128,14 +132,14 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
 69 
 70 	rss, err := feed.ToAtom()
 71 	if err != nil {
 72-		logger.Fatal(err)
 73+		logger.Error(err.Error())
 74 		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
 75 	}
 76 
 77 	w.Header().Add("Content-Type", "application/atom+xml")
 78 	_, err = w.Write([]byte(rss))
 79 	if err != nil {
 80-		logger.Error(err)
 81+		logger.Error(err.Error())
 82 	}
 83 }
 84 
 85@@ -226,14 +230,14 @@ func (h *AssetHandler) handle(w http.ResponseWriter) {
 86 		buf := new(strings.Builder)
 87 		_, err := io.Copy(buf, redirectFp)
 88 		if err != nil {
 89-			h.Logger.Error(err)
 90+			h.Logger.Error(err.Error())
 91 			http.Error(w, "cannot read _redirect file", http.StatusInternalServerError)
 92 			return
 93 		}
 94 
 95 		redirects, err = parseRedirectText(buf.String())
 96 		if err != nil {
 97-			h.Logger.Error(err)
 98+			h.Logger.Error(err.Error())
 99 		}
100 	}
101 
102@@ -266,10 +270,10 @@ func (h *AssetHandler) handle(w http.ResponseWriter) {
103 	}
104 
105 	if assetFilepath == "" {
106-		h.Logger.Infof(
107-			"asset not found in bucket: bucket:[%s], routes:[%s]",
108-			h.Bucket.Name,
109-			strings.Join(attempts, ", "),
110+		h.Logger.Info(
111+			"asset not found in bucket",
112+			"bucket", h.Bucket.Name,
113+			"routes", strings.Join(attempts, ", "),
114 		)
115 		http.Error(w, "404 not found", http.StatusNotFound)
116 		return
117@@ -285,7 +289,7 @@ func (h *AssetHandler) handle(w http.ResponseWriter) {
118 	_, err = io.Copy(w, contents)
119 
120 	if err != nil {
121-		h.Logger.Error(err)
122+		h.Logger.Error(err.Error())
123 	}
124 }
125 
126@@ -318,14 +322,14 @@ func ServeAsset(fname string, opts *storage.ImgProcessOpts, fromImgs bool, w htt
127 
128 	props, err := getProjectFromSubdomain(subdomain)
129 	if err != nil {
130-		logger.Info(err)
131+		logger.Info(err.Error(), "subdomain", subdomain, "filename", fname)
132 		http.Error(w, err.Error(), http.StatusNotFound)
133 		return
134 	}
135 
136 	user, err := dbpool.FindUserForName(props.Username)
137 	if err != nil {
138-		logger.Infof("user not found: %s", props.Username)
139+		logger.Info("user not found", "user", props.Username)
140 		http.Error(w, "user not found", http.StatusNotFound)
141 		return
142 	}
143@@ -347,7 +351,7 @@ func ServeAsset(fname string, opts *storage.ImgProcessOpts, fromImgs bool, w htt
144 	}
145 
146 	if err != nil {
147-		logger.Infof("bucket not found for %s", props.Username)
148+		logger.Info("bucket not found", "user", props.Username)
149 		http.Error(w, "bucket not found", http.StatusNotFound)
150 		return
151 	}
152@@ -377,7 +381,7 @@ func ImgAssetRequest(w http.ResponseWriter, r *http.Request) {
153 	opts, err := storage.UriToImgProcessOpts(imgOpts)
154 	if err != nil {
155 		errMsg := fmt.Sprintf("error processing img options: %s", err.Error())
156-		logger.Infof(errMsg)
157+		logger.Info(errMsg)
158 		http.Error(w, errMsg, http.StatusUnprocessableEntity)
159 	}
160 
161@@ -410,7 +414,8 @@ func StartApiServer() {
162 	cache := gocache.New(2*time.Minute, 5*time.Minute)
163 
164 	if err != nil {
165-		logger.Fatal(err)
166+		logger.Error(err.Error())
167+		return
168 	}
169 
170 	mainRoutes := []shared.Route{
171@@ -436,10 +441,11 @@ func StartApiServer() {
172 	router := http.HandlerFunc(handler)
173 
174 	portStr := fmt.Sprintf(":%s", cfg.Port)
175-	logger.Infof("Starting server on port %s", cfg.Port)
176-	logger.Infof("Subdomains enabled: %t", cfg.SubdomainsEnabled)
177-	logger.Infof("Domain: %s", cfg.Domain)
178-	logger.Infof("Email: %s", cfg.Email)
179-
180-	logger.Fatal(http.ListenAndServe(portStr, router))
181+	logger.Info(
182+		"Starting server on port",
183+		"port", cfg.Port,
184+		"domain", cfg.Domain,
185+		"email", cfg.Email,
186+	)
187+	logger.Error(http.ListenAndServe(portStr, router).Error())
188 }
M pgs/cli.go
+19, -19
  1@@ -4,13 +4,13 @@ import (
  2 	"errors"
  3 	"fmt"
  4 	"io"
  5+	"log/slog"
  6 	"os"
  7 	"path/filepath"
  8 
  9 	"github.com/picosh/pico/db"
 10 	"github.com/picosh/pico/shared"
 11 	"github.com/picosh/pico/shared/storage"
 12-	"go.uber.org/zap"
 13 )
 14 
 15 func getHelpText(userName, projectName string) string {
 16@@ -34,7 +34,7 @@ func getHelpText(userName, projectName string) string {
 17 }
 18 
 19 type CmdSessionLogger struct {
 20-	Log *zap.SugaredLogger
 21+	Log *slog.Logger
 22 }
 23 
 24 func (c *CmdSessionLogger) Write(out []byte) (int, error) {
 25@@ -65,7 +65,7 @@ type CmdSession interface {
 26 type Cmd struct {
 27 	User    *db.User
 28 	Session CmdSession
 29-	Log     *zap.SugaredLogger
 30+	Log     *slog.Logger
 31 	Store   storage.ObjectStorage
 32 	Dbpool  db.DB
 33 	Write   bool
 34@@ -85,7 +85,7 @@ func (c *Cmd) bail(err error) {
 35 	if err == nil {
 36 		return
 37 	}
 38-	c.Log.Error(err)
 39+	c.Log.Error(err.Error())
 40 	c.error(err)
 41 }
 42 
 43@@ -116,11 +116,11 @@ func (c *Cmd) RmProjectAssets(projectName string) error {
 44 
 45 	for _, file := range fileList {
 46 		intent := fmt.Sprintf("deleted (%s)", file.Name())
 47-		c.Log.Infof(
 48-			"(%s) attempting to delete (bucket: %s) (%s)",
 49-			c.User.Name,
 50-			bucket.Name,
 51-			file.Name(),
 52+		c.Log.Info(
 53+			"attempting to delete file",
 54+			"user", c.User.Name,
 55+			"bucket", bucket.Name,
 56+			"filename", file.Name(),
 57 		)
 58 		if c.Write {
 59 			err = c.Store.DeleteFile(
 60@@ -204,7 +204,7 @@ func (c *Cmd) ls() error {
 61 }
 62 
 63 func (c *Cmd) unlink(projectName string) error {
 64-	c.Log.Infof("user (%s) running `unlink` command with (%s)", c.User.Name, projectName)
 65+	c.Log.Info("user running `unlink` command", "user", c.User.Name, "project", projectName)
 66 	project, err := c.Dbpool.FindProjectByName(c.User.ID, projectName)
 67 	if err != nil {
 68 		return errors.Join(err, fmt.Errorf("project (%s) does not exit", projectName))
 69@@ -220,7 +220,7 @@ func (c *Cmd) unlink(projectName string) error {
 70 }
 71 
 72 func (c *Cmd) link(projectName, linkTo string) error {
 73-	c.Log.Infof("user (%s) running `link` command with (%s) (%s)", c.User.Name, projectName, linkTo)
 74+	c.Log.Info("user running `link` command", "user", c.User.Name, "project", projectName, "link", linkTo)
 75 
 76 	projectDir := linkTo
 77 	_, err := c.Dbpool.FindProjectByName(c.User.ID, linkTo)
 78@@ -233,13 +233,13 @@ func (c *Cmd) link(projectName, linkTo string) error {
 79 	projectID := ""
 80 	if err == nil {
 81 		projectID = project.ID
 82-		c.Log.Infof("user (%s) already has project (%s), updating", c.User.Name, projectName)
 83+		c.Log.Info("user already has project, updating", "user", c.User.Name, "project", projectName)
 84 		err = c.Dbpool.LinkToProject(c.User.ID, project.ID, projectDir, c.Write)
 85 		if err != nil {
 86 			return err
 87 		}
 88 	} else {
 89-		c.Log.Infof("user (%s) has no project record (%s), creating", c.User.Name, projectName)
 90+		c.Log.Info("user has no project record, creating", "user", c.User.Name, "project", projectName)
 91 		if !c.Write {
 92 			out := fmt.Sprintf("(%s) cannot create a new project without `--write` permission, aborting", projectName)
 93 			c.output(out)
 94@@ -252,7 +252,7 @@ func (c *Cmd) link(projectName, linkTo string) error {
 95 		projectID = id
 96 	}
 97 
 98-	c.Log.Infof("user (%s) linking (%s) to (%s)", c.User.Name, projectName, projectDir)
 99+	c.Log.Info("user linking", "user", c.User.Name, "project", projectName, "projectDir", projectDir)
100 	err = c.Dbpool.LinkToProject(c.User.ID, projectID, projectDir, c.Write)
101 	if err != nil {
102 		return err
103@@ -297,7 +297,7 @@ func (c *Cmd) depends(projectName string) error {
104 // delete all the projects and associated assets matching prefix
105 // but keep the latest N records.
106 func (c *Cmd) prune(prefix string, keepNumLatest int) error {
107-	c.Log.Infof("user (%s) running `clean` command for (%s)", c.User.Name, prefix)
108+	c.Log.Info("user running `clean` command", "user", c.User.Name, "prefix", prefix)
109 	c.output(fmt.Sprintf("searching for projects that match prefix (%s) and are not linked to other projects", prefix))
110 
111 	if prefix == "" || prefix == "*" {
112@@ -357,7 +357,7 @@ func (c *Cmd) prune(prefix string, keepNumLatest int) error {
113 		c.output(out)
114 
115 		if c.Write {
116-			c.Log.Infof("(%s) removing", project.Name)
117+			c.Log.Info("removing project", "project", project.Name)
118 			err = c.Dbpool.RemoveProject(project.ID)
119 			if err != nil {
120 				return err
121@@ -375,10 +375,10 @@ func (c *Cmd) prune(prefix string, keepNumLatest int) error {
122 }
123 
124 func (c *Cmd) rm(projectName string) error {
125-	c.Log.Infof("user (%s) running `rm` command for (%s)", c.User.Name, projectName)
126+	c.Log.Info("user running `rm` command", "user", c.User.Name, "project", projectName)
127 	project, err := c.Dbpool.FindProjectByName(c.User.ID, projectName)
128 	if err == nil {
129-		c.Log.Infof("found project (%s) (%s), checking dependencies", projectName, project.ID)
130+		c.Log.Info("found project, checking dependencies", "project", projectName, "projectID", project.ID)
131 
132 		links, err := c.Dbpool.FindProjectLinks(c.User.ID, projectName)
133 		if err != nil {
134@@ -393,7 +393,7 @@ func (c *Cmd) rm(projectName string) error {
135 		out := fmt.Sprintf("(%s) removing", project.Name)
136 		c.output(out)
137 		if c.Write {
138-			c.Log.Infof("(%s) removing", project.Name)
139+			c.Log.Info("removing project", "project", project.Name)
140 			err = c.Dbpool.RemoveProject(project.ID)
141 			if err != nil {
142 				return err
M pgs/cms.go
+4, -4
 1@@ -93,7 +93,7 @@ func CmsMiddleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
 2 		}
 3 		key, err := util.KeyText(s)
 4 		if err != nil {
 5-			logger.Error(err)
 6+			logger.Error(err.Error())
 7 		}
 8 
 9 		sshUser := s.User()
10@@ -108,7 +108,7 @@ func CmsMiddleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
11 		}
12 
13 		if err != nil {
14-			logger.Fatal(err)
15+			logger.Error(err.Error())
16 		}
17 
18 		m := model{
19@@ -171,14 +171,14 @@ func (m model) findUser() (*db.User, error) {
20 	var user *db.User
21 
22 	if m.sshUser == "new" {
23-		logger.Infof("User requesting to register account")
24+		logger.Info("user requesting to register account", "user", user.Name)
25 		return nil, nil
26 	}
27 
28 	user, err := m.dbpool.FindUserForKey(m.sshUser, m.publicKey)
29 
30 	if err != nil {
31-		logger.Error(err)
32+		logger.Error(err.Error())
33 		// we only want to throw an error for specific cases
34 		if errors.Is(err, &db.ErrMultiplePublicKeys{}) {
35 			return nil, err
M pgs/ssh.go
+7, -5
 1@@ -76,7 +76,8 @@ func StartSshServer() {
 2 	}
 3 
 4 	if err != nil {
 5-		logger.Fatal(err)
 6+		logger.Error(err.Error())
 7+		return
 8 	}
 9 
10 	handler := uploadassets.NewUploadAssetHandler(
11@@ -97,15 +98,16 @@ func StartSshServer() {
12 		),
13 	)
14 	if err != nil {
15-		logger.Fatal(err)
16+		logger.Error(err.Error())
17+		return
18 	}
19 
20 	done := make(chan os.Signal, 1)
21 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
22-	logger.Infof("Starting SSH server on %s:%s", host, port)
23+	logger.Info("Starting SSH server on", "host", host, "port", port)
24 	go func() {
25 		if err = s.ListenAndServe(); err != nil {
26-			logger.Fatal(err)
27+			logger.Error(err.Error())
28 		}
29 	}()
30 
31@@ -114,6 +116,6 @@ func StartSshServer() {
32 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
33 	defer func() { cancel() }()
34 	if err := s.Shutdown(ctx); err != nil {
35-		logger.Fatal(err)
36+		logger.Error(err.Error())
37 	}
38 }
M pgs/wish.go
+1, -1
1@@ -83,7 +83,7 @@ func WishMiddleware(handler *uploadassets.UploadAssetHandler) wish.Middleware {
2 				}
3 			}
4 
5-			log.Infof("pgs middleware detected command: %s", args)
6+			log.Info("pgs middleware detected command", "args", args)
7 			projectName := strings.TrimSpace(args[1])
8 
9 			if projectName == "--write" {
M prose/api.go
+44, -42
  1@@ -131,13 +131,13 @@ func blogStyleHandler(w http.ResponseWriter, r *http.Request) {
  2 
  3 	user, err := dbpool.FindUserForName(username)
  4 	if err != nil {
  5-		logger.Infof("blog not found: %s", username)
  6+		logger.Info("blog not found", "user", username)
  7 		http.Error(w, "blog not found", http.StatusNotFound)
  8 		return
  9 	}
 10 	styles, err := dbpool.FindPostWithFilename("_styles.css", user.ID, cfg.Space)
 11 	if err != nil {
 12-		logger.Infof("css not found for: %s", username)
 13+		logger.Info("css not found", "user", username)
 14 		http.Error(w, "css not found", http.StatusNotFound)
 15 		return
 16 	}
 17@@ -146,7 +146,7 @@ func blogStyleHandler(w http.ResponseWriter, r *http.Request) {
 18 
 19 	_, err = w.Write([]byte(styles.Text))
 20 	if err != nil {
 21-		logger.Error(err)
 22+		logger.Error(err.Error())
 23 		http.Error(w, "server error", 500)
 24 	}
 25 }
 26@@ -159,7 +159,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 27 
 28 	user, err := dbpool.FindUserForName(username)
 29 	if err != nil {
 30-		logger.Infof("blog not found: %s", username)
 31+		logger.Info("blog not found", "user", username)
 32 		http.Error(w, "blog not found", http.StatusNotFound)
 33 		return
 34 	}
 35@@ -176,7 +176,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 36 	posts = p.Data
 37 
 38 	if err != nil {
 39-		logger.Error(err)
 40+		logger.Error(err.Error())
 41 		http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
 42 		return
 43 	}
 44@@ -190,7 +190,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 45 	curl := shared.CreateURLFromRequest(cfg, r)
 46 
 47 	if err != nil {
 48-		logger.Error(err)
 49+		logger.Error(err.Error())
 50 		http.Error(w, err.Error(), http.StatusInternalServerError)
 51 		return
 52 	}
 53@@ -207,7 +207,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 54 	if err == nil {
 55 		parsedText, err := shared.ParseText(readme.Text)
 56 		if err != nil {
 57-			logger.Error(err)
 58+			logger.Error(err.Error())
 59 		}
 60 		headerTxt.Bio = parsedText.Description
 61 		headerTxt.Layout = parsedText.Layout
 62@@ -277,7 +277,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
 63 
 64 	err = ts.Execute(w, data)
 65 	if err != nil {
 66-		logger.Error(err)
 67+		logger.Error(err.Error())
 68 		http.Error(w, err.Error(), http.StatusInternalServerError)
 69 	}
 70 }
 71@@ -300,14 +300,14 @@ func postRawHandler(w http.ResponseWriter, r *http.Request) {
 72 
 73 	user, err := dbpool.FindUserForName(username)
 74 	if err != nil {
 75-		logger.Infof("blog not found: %s", username)
 76+		logger.Info("blog not found", "user", username)
 77 		http.Error(w, "blog not found", http.StatusNotFound)
 78 		return
 79 	}
 80 
 81 	post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
 82 	if err != nil {
 83-		logger.Infof("post not found")
 84+		logger.Info("post not found")
 85 		http.Error(w, "post not found", http.StatusNotFound)
 86 		return
 87 	}
 88@@ -316,7 +316,7 @@ func postRawHandler(w http.ResponseWriter, r *http.Request) {
 89 
 90 	_, err = w.Write([]byte(post.Text))
 91 	if err != nil {
 92-		logger.Error(err)
 93+		logger.Error(err.Error())
 94 		http.Error(w, "server error", 500)
 95 	}
 96 }
 97@@ -339,7 +339,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 98 
 99 	user, err := dbpool.FindUserForName(username)
100 	if err != nil {
101-		logger.Infof("blog not found: %s", username)
102+		logger.Info("blog not found", "user", username)
103 		http.Error(w, "blog not found", http.StatusNotFound)
104 		return
105 	}
106@@ -356,7 +356,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
107 	if err == nil {
108 		parsedText, err := shared.ParseText(post.Text)
109 		if err != nil {
110-			logger.Error(err)
111+			logger.Error(err.Error())
112 		}
113 
114 		// we need the blog name from the readme unfortunately
115@@ -364,7 +364,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
116 		if err == nil {
117 			readmeParsed, err := shared.ParseText(readme.Text)
118 			if err != nil {
119-				logger.Error(err)
120+				logger.Error(err.Error())
121 			}
122 			if readmeParsed.MetaData.Title != "" {
123 				blogName = readmeParsed.MetaData.Title
124@@ -394,7 +394,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
125 		if err == nil {
126 			footerParsed, err := shared.ParseText(footer.Text)
127 			if err != nil {
128-				logger.Error(err)
129+				logger.Error(err.Error())
130 			}
131 			footerHTML = template.HTML(footerParsed.Html)
132 		}
133@@ -403,7 +403,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
134 		if isRequestTrackable(r) {
135 			_, err := dbpool.AddViewCount(post.ID)
136 			if err != nil {
137-				logger.Error(err)
138+				logger.Error(err.Error())
139 			}
140 		}
141 
142@@ -456,7 +456,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
143 			Contents:     "Oops!  we can't seem to find this post.",
144 			Unlisted:     true,
145 		}
146-		logger.Infof("post not found %s/%s", username, slug)
147+		logger.Info("post not found", "user", username, "slug", slug)
148 	}
149 
150 	ts, err := shared.RenderTemplate(cfg, []string{
151@@ -469,7 +469,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
152 
153 	err = ts.Execute(w, data)
154 	if err != nil {
155-		logger.Error(err)
156+		logger.Error(err.Error())
157 		http.Error(w, err.Error(), http.StatusInternalServerError)
158 	}
159 }
160@@ -490,7 +490,7 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
161 	}
162 
163 	if err != nil {
164-		logger.Error(err)
165+		logger.Error(err.Error())
166 		http.Error(w, err.Error(), http.StatusInternalServerError)
167 		return
168 	}
169@@ -521,7 +521,7 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
170 
171 	tags, err := dbpool.FindPopularTags(cfg.Space)
172 	if err != nil {
173-		logger.Error(err)
174+		logger.Error(err.Error())
175 	}
176 
177 	data := ReadPageData{
178@@ -551,7 +551,7 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
179 
180 	err = ts.Execute(w, data)
181 	if err != nil {
182-		logger.Error(err)
183+		logger.Error(err.Error())
184 		http.Error(w, err.Error(), http.StatusInternalServerError)
185 	}
186 }
187@@ -564,7 +564,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
188 
189 	user, err := dbpool.FindUserForName(username)
190 	if err != nil {
191-		logger.Infof("rss feed not found: %s", username)
192+		logger.Info("rss feed not found", "user", username)
193 		http.Error(w, "rss feed not found", http.StatusNotFound)
194 		return
195 	}
196@@ -581,14 +581,14 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
197 	posts = p.Data
198 
199 	if err != nil {
200-		logger.Error(err)
201+		logger.Error(err.Error())
202 		http.Error(w, err.Error(), http.StatusInternalServerError)
203 		return
204 	}
205 
206 	ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
207 	if err != nil {
208-		logger.Error(err)
209+		logger.Error(err.Error())
210 		http.Error(w, err.Error(), http.StatusInternalServerError)
211 		return
212 	}
213@@ -601,7 +601,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
214 	if err == nil {
215 		parsedText, err := shared.ParseText(readme.Text)
216 		if err != nil {
217-			logger.Error(err)
218+			logger.Error(err.Error())
219 		}
220 		if parsedText.Title != "" {
221 			headerTxt.Title = parsedText.Title
222@@ -631,7 +631,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
223 		}
224 		parsed, err := shared.ParseText(post.Text)
225 		if err != nil {
226-			logger.Error(err)
227+			logger.Error(err.Error())
228 		}
229 
230 		footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space)
231@@ -639,7 +639,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
232 		if err == nil {
233 			footerParsed, err := shared.ParseText(footer.Text)
234 			if err != nil {
235-				logger.Error(err)
236+				logger.Error(err.Error())
237 			}
238 			footerHTML = footerParsed.Html
239 		}
240@@ -673,14 +673,14 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
241 
242 	rss, err := feed.ToAtom()
243 	if err != nil {
244-		logger.Fatal(err)
245+		logger.Error(err.Error())
246 		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
247 	}
248 
249 	w.Header().Add("Content-Type", "application/atom+xml")
250 	_, err = w.Write([]byte(rss))
251 	if err != nil {
252-		logger.Error(err)
253+		logger.Error(err.Error())
254 	}
255 }
256 
257@@ -691,14 +691,14 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
258 
259 	pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, cfg.Space)
260 	if err != nil {
261-		logger.Error(err)
262+		logger.Error(err.Error())
263 		http.Error(w, err.Error(), http.StatusInternalServerError)
264 		return
265 	}
266 
267 	ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
268 	if err != nil {
269-		logger.Error(err)
270+		logger.Error(err.Error())
271 		http.Error(w, err.Error(), http.StatusInternalServerError)
272 		return
273 	}
274@@ -717,7 +717,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
275 	for _, post := range pager.Data {
276 		parsed, err := shared.ParseText(post.Text)
277 		if err != nil {
278-			logger.Error(err)
279+			logger.Error(err.Error())
280 		}
281 
282 		var tpl bytes.Buffer
283@@ -754,14 +754,14 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
284 
285 	rss, err := feed.ToAtom()
286 	if err != nil {
287-		logger.Fatal(err)
288+		logger.Error(err.Error())
289 		http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
290 	}
291 
292 	w.Header().Add("Content-Type", "application/atom+xml")
293 	_, err = w.Write([]byte(rss))
294 	if err != nil {
295-		logger.Error(err)
296+		logger.Error(err.Error())
297 	}
298 }
299 
300@@ -772,14 +772,14 @@ func serveFile(file string, contentType string) http.HandlerFunc {
301 
302 		contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
303 		if err != nil {
304-			logger.Error(err)
305+			logger.Error(err.Error())
306 			http.Error(w, "file not found", 404)
307 		}
308 		w.Header().Add("Content-Type", contentType)
309 
310 		_, err = w.Write(contents)
311 		if err != nil {
312-			logger.Error(err)
313+			logger.Error(err.Error())
314 			http.Error(w, "server error", 500)
315 		}
316 	}
317@@ -882,7 +882,7 @@ func StartApiServer() {
318 	cache := gocache.New(2*time.Minute, 5*time.Minute)
319 
320 	if err != nil {
321-		logger.Fatal(err)
322+		logger.Error(err.Error())
323 	}
324 
325 	staticRoutes := createStaticRoutes()
326@@ -898,10 +898,12 @@ func StartApiServer() {
327 	router := http.HandlerFunc(handler)
328 
329 	portStr := fmt.Sprintf(":%s", cfg.Port)
330-	logger.Infof("Starting server on port %s", cfg.Port)
331-	logger.Infof("Subdomains enabled: %t", cfg.SubdomainsEnabled)
332-	logger.Infof("Domain: %s", cfg.Domain)
333-	logger.Infof("Email: %s", cfg.Email)
334+	logger.Info(
335+		"Starting server on port",
336+		"port", cfg.Port,
337+		"domain", cfg.Domain,
338+		"email", cfg.Email,
339+	)
340 
341-	logger.Fatal(http.ListenAndServe(portStr, router))
342+	logger.Error(http.ListenAndServe(portStr, router).Error())
343 }
M prose/ssh.go
+7, -5
 1@@ -81,7 +81,8 @@ func StartSshServer() {
 2 	}
 3 
 4 	if err != nil {
 5-		logger.Fatal(err)
 6+		logger.Error(err.Error())
 7+		return
 8 	}
 9 
10 	fileMap := map[string]filehandlers.ReadWriteHandler{
11@@ -103,15 +104,16 @@ func StartSshServer() {
12 		),
13 	)
14 	if err != nil {
15-		logger.Fatal(err)
16+		logger.Error(err.Error())
17+		return
18 	}
19 
20 	done := make(chan os.Signal, 1)
21 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
22-	logger.Infof("Starting SSH server on %s:%s", host, port)
23+	logger.Info("Starting SSH server", "host", host, "port", port)
24 	go func() {
25 		if err = s.ListenAndServe(); err != nil {
26-			logger.Fatal(err)
27+			logger.Error(err.Error())
28 		}
29 	}()
30 
31@@ -120,6 +122,6 @@ func StartSshServer() {
32 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
33 	defer func() { cancel() }()
34 	if err := s.Shutdown(ctx); err != nil {
35-		logger.Fatal(err)
36+		logger.Error(err.Error())
37 	}
38 }
M shared/api.go
+5, -6
 1@@ -11,14 +11,13 @@ import (
 2 func CheckHandler(w http.ResponseWriter, r *http.Request) {
 3 	dbpool := GetDB(r)
 4 	cfg := GetCfg(r)
 5-	logger := GetLogger(r)
 6 
 7 	if cfg.IsCustomdomains() {
 8 		hostDomain := r.URL.Query().Get("domain")
 9 		appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
10 
11 		if !strings.Contains(hostDomain, appDomain) {
12-			subdomain := GetCustomDomain(logger, hostDomain, cfg.Space)
13+			subdomain := GetCustomDomain(hostDomain, cfg.Space)
14 			if subdomain != "" {
15 				u, err := dbpool.FindUserForName(subdomain)
16 				if u != nil && err == nil {
17@@ -49,7 +48,7 @@ func ServeFile(file string, contentType string) http.HandlerFunc {
18 
19 		contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
20 		if err != nil {
21-			logger.Error(err)
22+			logger.Error(err.Error())
23 			http.Error(w, "file not found", 404)
24 		}
25 
26@@ -57,7 +56,7 @@ func ServeFile(file string, contentType string) http.HandlerFunc {
27 
28 		_, err = w.Write(contents)
29 		if err != nil {
30-			logger.Error(err)
31+			logger.Error(err.Error())
32 		}
33 	}
34 }
35@@ -104,7 +103,7 @@ func CreatePageHandler(fname string) http.HandlerFunc {
36 		ts, err := RenderTemplate(cfg, []string{cfg.StaticPath(fname)})
37 
38 		if err != nil {
39-			logger.Error(err)
40+			logger.Error(err.Error())
41 			http.Error(w, err.Error(), http.StatusInternalServerError)
42 			return
43 		}
44@@ -114,7 +113,7 @@ func CreatePageHandler(fname string) http.HandlerFunc {
45 		}
46 		err = ts.Execute(w, data)
47 		if err != nil {
48-			logger.Error(err)
49+			logger.Error(err.Error())
50 			http.Error(w, err.Error(), http.StatusInternalServerError)
51 		}
52 	}
M shared/config.go
+8, -18
 1@@ -3,14 +3,14 @@ package shared
 2 import (
 3 	"fmt"
 4 	"html/template"
 5-	"log"
 6+	"log/slog"
 7 	"net/http"
 8 	"net/url"
 9+	"os"
10 	"path"
11 	"strings"
12 
13 	"github.com/picosh/pico/wish/cms/config"
14-	"go.uber.org/zap"
15 )
16 
17 type SitePageData struct {
18@@ -251,21 +251,11 @@ func (c *ConfigSite) AssetURL(username, projectName, fpath string) string {
19 	)
20 }
21 
22-func CreateLogger(debug bool) *zap.SugaredLogger {
23-	var (
24-		err    error
25-		logger *zap.Logger
26-	)
27-
28-	if debug {
29-		logger, err = zap.NewDevelopment()
30-	} else {
31-		logger, err = zap.NewProduction()
32+func CreateLogger(debug bool) *slog.Logger {
33+	opts := &slog.HandlerOptions{
34+		AddSource: true,
35 	}
36-
37-	if err != nil {
38-		log.Fatal(err)
39-	}
40-
41-	return logger.Sugar()
42+	return slog.New(
43+		slog.NewJSONHandler(os.Stdout, opts),
44+	)
45 }
M shared/router.go
+6, -7
 1@@ -3,6 +3,7 @@ package shared
 2 import (
 3 	"context"
 4 	"fmt"
 5+	"log/slog"
 6 	"net"
 7 	"net/http"
 8 	"net/http/pprof"
 9@@ -12,7 +13,6 @@ import (
10 	"github.com/patrickmn/go-cache"
11 	"github.com/picosh/pico/db"
12 	"github.com/picosh/pico/shared/storage"
13-	"go.uber.org/zap"
14 )
15 
16 type Route struct {
17@@ -46,7 +46,7 @@ func CreatePProfRoutes(routes []Route) []Route {
18 
19 type ServeFn func(http.ResponseWriter, *http.Request)
20 
21-func CreateServe(routes []Route, subdomainRoutes []Route, cfg *ConfigSite, dbpool db.DB, st storage.ObjectStorage, logger *zap.SugaredLogger, cache *cache.Cache) ServeFn {
22+func CreateServe(routes []Route, subdomainRoutes []Route, cfg *ConfigSite, dbpool db.DB, st storage.ObjectStorage, logger *slog.Logger, cache *cache.Cache) ServeFn {
23 	return func(w http.ResponseWriter, r *http.Request) {
24 		var allow []string
25 		var subdomain string
26@@ -63,7 +63,7 @@ func CreateServe(routes []Route, subdomainRoutes []Route, cfg *ConfigSite, dbpoo
27 						curRoutes = subdomainRoutes
28 					}
29 				} else {
30-					subdomain = GetCustomDomain(logger, hostDomain, cfg.Space)
31+					subdomain = GetCustomDomain(hostDomain, cfg.Space)
32 					if subdomain != "" {
33 						curRoutes = subdomainRoutes
34 					}
35@@ -110,8 +110,8 @@ func GetCfg(r *http.Request) *ConfigSite {
36 	return r.Context().Value(ctxCfg{}).(*ConfigSite)
37 }
38 
39-func GetLogger(r *http.Request) *zap.SugaredLogger {
40-	return r.Context().Value(ctxLoggerKey{}).(*zap.SugaredLogger)
41+func GetLogger(r *http.Request) *slog.Logger {
42+	return r.Context().Value(ctxLoggerKey{}).(*slog.Logger)
43 }
44 
45 func GetCache(r *http.Request) *cache.Cache {
46@@ -138,11 +138,10 @@ func GetSubdomain(r *http.Request) string {
47 	return r.Context().Value(ctxSubdomainKey{}).(string)
48 }
49 
50-func GetCustomDomain(logger *zap.SugaredLogger, host string, space string) string {
51+func GetCustomDomain(host string, space string) string {
52 	txt := fmt.Sprintf("_%s.%s", space, host)
53 	records, err := net.LookupTXT(txt)
54 	if err != nil {
55-		logger.Info(err)
56 		return ""
57 	}
58 
M wish/cms/cms.go
+4, -4
 1@@ -99,7 +99,7 @@ func Middleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
 2 		}
 3 		key, err := util.KeyText(s)
 4 		if err != nil {
 5-			logger.Error(err)
 6+			logger.Error(err.Error())
 7 		}
 8 
 9 		sshUser := s.User()
10@@ -114,7 +114,7 @@ func Middleware(cfg *config.ConfigCms, urls config.ConfigURL) bm.Handler {
11 		}
12 
13 		if err != nil {
14-			logger.Fatal(err)
15+			logger.Error(err.Error())
16 		}
17 
18 		m := model{
19@@ -178,14 +178,14 @@ func (m model) findUser() (*db.User, error) {
20 	var user *db.User
21 
22 	if m.sshUser == "new" {
23-		logger.Infof("User requesting to register account")
24+		logger.Info("user requesting to register account", "user", user.Name)
25 		return nil, nil
26 	}
27 
28 	user, err := m.dbpool.FindUserForKey(m.sshUser, m.publicKey)
29 
30 	if err != nil {
31-		logger.Error(err)
32+		logger.Error(err.Error())
33 		// we only want to throw an error for specific cases
34 		if errors.Is(err, &db.ErrMultiplePublicKeys{}) {
35 			return nil, err
M wish/cms/config/config.go
+2, -4
 1@@ -1,8 +1,6 @@
 2 package config
 3 
 4-import (
 5-	"go.uber.org/zap"
 6-)
 7+import "log/slog"
 8 
 9 type ConfigURL interface {
10 	BlogURL(username string) string
11@@ -24,7 +22,7 @@ type ConfigCms struct {
12 	Space         string
13 	AllowedExt    []string
14 	HiddenPosts   []string
15-	Logger        *zap.SugaredLogger
16+	Logger        *slog.Logger
17 	AllowRegister bool
18 	MaxSize       uint64
19 	MaxAssetSize  int64
M wish/cms/ui/keys/keys.go
+1, -1
1@@ -411,7 +411,7 @@ func unlinkKey(m Model) tea.Cmd {
2 func deleteAccount(m Model) tea.Cmd {
3 	return func() tea.Msg {
4 		id := m.keys[m.getSelectedIndex()].UserID
5-		m.cfg.Logger.Infof("User (%s) requested account deletion (%s)", m.user.Name, id)
6+		m.cfg.Logger.Info("user requested account deletion", "user", m.user.Name, "id", id)
7 		err := m.dbpool.RemoveUsers([]string{id})
8 		if err != nil {
9 			return errMsg{err}
M wish/cms/ui/posts/posts.go
+2, -3
 1@@ -2,6 +2,7 @@ package posts
 2 
 3 import (
 4 	"errors"
 5+	"log/slog"
 6 
 7 	pager "github.com/charmbracelet/bubbles/paginator"
 8 	"github.com/charmbracelet/bubbles/spinner"
 9@@ -11,8 +12,6 @@ import (
10 	"github.com/picosh/pico/shared/storage"
11 	"github.com/picosh/pico/wish/cms/config"
12 	"github.com/picosh/pico/wish/cms/ui/common"
13-
14-	"go.uber.org/zap"
15 )
16 
17 const keysPerPage = 1
18@@ -62,7 +61,7 @@ type Model struct {
19 	Exit    bool
20 	Quit    bool
21 	spinner spinner.Model
22-	logger  *zap.SugaredLogger
23+	logger  *slog.Logger
24 }
25 
26 // getSelectedIndex returns the index of the cursor in relation to the total