cerrado @ 3d8637838e9ccfcb56899842945e760f337428b0

  1diff --git a/config.example.scfg b/config.example.scfg
  2index eda4f38d14d06608e6447089bbf13c5416092d65..1e3180f17834707170c018db8e31909eabda4cfe 100644
  3--- a/config.example.scfg
  4+++ b/config.example.scfg
  5@@ -1,3 +1,4 @@
  6+root-readme /srv/git/README.md
  7 scan /srv/git/ {
  8     public true
  9 }
 10diff --git a/go.mod b/go.mod
 11index 2122f6bb0e88ee0dacf217829b1693e79fb19e81..5bd4d7647b43cb737a873b5636a162a1ea25df69 100644
 12--- a/go.mod
 13+++ b/go.mod
 14@@ -5,6 +5,8 @@
 15 require (
 16 	git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082
 17 	github.com/alecthomas/chroma/v2 v2.13.0
 18+	github.com/go-git/go-git/v5 v5.12.0
 19+	github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
 20 	github.com/google/go-cmp v0.6.0
 21 	github.com/valyala/quicktemplate v1.7.0
 22 	golang.org/x/sync v0.7.0
 23@@ -20,7 +22,6 @@ 	github.com/dlclark/regexp2 v1.11.0 // indirect
 24 	github.com/emirpasic/gods v1.18.1 // indirect
 25 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 26 	github.com/go-git/go-billy/v5 v5.5.0 // indirect
 27-	github.com/go-git/go-git/v5 v5.12.0 // indirect
 28 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 29 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 30 	github.com/kevinburke/ssh_config v1.2.0 // indirect
 31diff --git a/go.sum b/go.sum
 32index ba2d937337ab28a5ed189010a1aad51addbdac36..69c34b73d113fa1f0ae49c5068a2517c3ee91725 100644
 33--- a/go.sum
 34+++ b/go.sum
 35@@ -15,6 +15,10 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
 36 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 37 github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 38 github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 39+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 40+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 41+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 42+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 43 github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 44 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 45 github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
 46@@ -26,17 +30,25 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 47 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 48 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 49 github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 50+github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
 51+github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
 52 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
 53 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 54+github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
 55+github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
 56 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
 57 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
 58 github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
 59 github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
 60+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 61+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
 62 github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
 63 github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
 64 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 65 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 66 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 67+github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 h1:yEt5djSYb4iNtmV9iJGVday+i4e9u6Mrn5iP64HH5QM=
 68+github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
 69 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 70 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 71 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 72@@ -48,12 +60,22 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 73 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 74 github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 75 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 76+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 77+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 78 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 79 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 80+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 81+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 82+github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
 83+github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
 84 github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
 85 github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
 86+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 87 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 88+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 89 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 90+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 91+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 92 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
 93 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
 94 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 95@@ -62,6 +84,8 @@ github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
 96 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 97 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 98 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 99+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
100+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
101 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
102 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
103 github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
104@@ -119,6 +143,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
105 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
106 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
107 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
108+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
109+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
110 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
111 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
112 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
113@@ -126,6 +152,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
114 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
115 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
116 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
117+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
118+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
119 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
120 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
121 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
122@@ -135,8 +163,11 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
123 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
124 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
125 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
126+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
127+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
128 gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
129 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
130 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
131 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
132+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
133 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
134diff --git a/main.go b/main.go
135index 644b96caba69614ba0b5c8ff33fd1280cba9eaf7..76da07bd4f55d968e2d8de6fde919aaca7325301 100644
136--- a/main.go
137+++ b/main.go
138@@ -58,9 +58,11 @@ 	gitService := service.NewGitService(gitServer)
139 
140 	//handlers
141 	gitHandler := handler.NewGitHandler(gitService)
142+	aboutHandler := handler.NewAboutHandler(config.RootReadme)
143 
144 	mux.Handle("/static/", staticHandler)
145 	mux.HandleFunc("/config", handler.ConfigFile(*configPath))
146+	mux.HandleFunc("/about", aboutHandler.About)
147 	mux.HandleFunc("/", gitHandler.List)
148 	serverTask := worker.NewServerTask(&http.Server{Handler: mux, Addr: "0.0.0.0:8080"})
149 
150diff --git a/pkg/config/config.go b/pkg/config/config.go
151index 776c69888056005fa086fd14509354449665baa4..9b6accea2b385fa1180aec54df7e0a64bb3ee6fe 100644
152--- a/pkg/config/config.go
153+++ b/pkg/config/config.go
154@@ -16,7 +16,8 @@ 		Public bool
155 	}
156 
157 	Configuration struct {
158-		Scan *Scan
159+		Scan       *Scan
160+		RootReadme string
161 	}
162 )
163 
164@@ -33,6 +34,11 @@ 	if err != nil {
165 		return nil, err
166 	}
167 
168+	err = setRootReadme(block, &config.RootReadme)
169+	if err != nil {
170+		return nil, err
171+	}
172+
173 	return config, nil
174 }
175 
176@@ -42,7 +48,13 @@ 		Scan: &Scan{
177 			Public: true,
178 			Path:   "",
179 		},
180+		RootReadme: "",
181 	}
182+}
183+
184+func setRootReadme(block scfg.Block, readme *string) error {
185+	scanDir := block.Get("root-readme")
186+	return setString(scanDir, readme)
187 }
188 
189 func setScan(block scfg.Block, scan *Scan) error {
190diff --git a/pkg/handler/about.go b/pkg/handler/about.go
191new file mode 100644
192index 0000000000000000000000000000000000000000..3ab2de880549e94be04543d4a0d4698d5d87f2b5
193--- /dev/null
194+++ b/pkg/handler/about.go
195@@ -0,0 +1,51 @@
196+package handler
197+
198+import (
199+	"io"
200+	"log/slog"
201+	"net/http"
202+	"os"
203+
204+	"github.com/gomarkdown/markdown"
205+	"github.com/gomarkdown/markdown/html"
206+	"github.com/gomarkdown/markdown/parser"
207+
208+	"git.gabrielgio.me/cerrado/templates"
209+)
210+
211+type AboutHandler struct {
212+	readmePath string
213+}
214+
215+func NewAboutHandler(readmePath string) *AboutHandler {
216+	return &AboutHandler{readmePath}
217+}
218+
219+func (g *AboutHandler) About(w http.ResponseWriter, _ *http.Request) {
220+	f, err := os.Open(g.readmePath)
221+	if err != nil {
222+		slog.Error("Error loading readme file", "error", err)
223+		return
224+	}
225+
226+	bs, err := io.ReadAll(f)
227+	if err != nil {
228+		slog.Error("Error reading readme file bytes", "error", err)
229+		return
230+	}
231+
232+	extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
233+	p := parser.NewWithExtensions(extensions)
234+	doc := p.Parse(bs)
235+
236+	htmlFlags := html.CommonFlags | html.HrefTargetBlank
237+	opts := html.RendererOptions{Flags: htmlFlags}
238+	renderer := html.NewRenderer(opts)
239+
240+	bs = markdown.Render(doc, renderer)
241+
242+	gitList := &templates.HelloPage{
243+		Body: bs,
244+	}
245+	templates.WritePageTemplate(w, gitList)
246+}
247diff --git a/pkg/handler/status.go b/pkg/handler/status.go
248index 1ca7f706241b1bce139bef2eb71b704b9e1c76aa..9baac2c47a3fce61a82cc9451ecc92e8de179c87 100644
249--- a/pkg/handler/status.go
250+++ b/pkg/handler/status.go
251@@ -54,7 +54,7 @@ 			return
252 		}
253 
254 		hello := &templates.HelloPage{
255-			Body: code.String(),
256+			Body: code.Bytes(),
257 		}
258 
259 		templates.WritePageTemplate(w, hello)
260diff --git a/pkg/u/util.go b/pkg/u/util.go
261new file mode 100644
262index 0000000000000000000000000000000000000000..97065395338eceba128b818410c1eec00290f616
263--- /dev/null
264+++ b/pkg/u/util.go
265@@ -0,0 +1,5 @@
266+package u
267+
268+func Ptr[T any](v T) *T {
269+	return &v
270+}
271diff --git a/templates/base.qtpl b/templates/base.qtpl
272index 6654f42a73f2ba9287aa5f6614891d2b6a75f9a9..444afc639b5d8bc1c520d7b62edfe341c7d1bc06 100644
273--- a/templates/base.qtpl
274+++ b/templates/base.qtpl
275@@ -32,11 +32,12 @@         <meta name="viewport" content="width=device-width, initial-scale=1" />
276     </head>
277     <body>
278         <nav class="container navbar navbar-expand-sm">
279-          <ul class="navbar-nav">
280-            <li class="nav-link" href="/git">git</li>
281-            <li class="nav-link" href="/list">list</li>
282-            <li class="nav-link" href="/about">about</li>
283-          </ul>
284+          <div class="navbar-nav">
285+            <a class="nav-link" href="/git">git</a>
286+            <a class="nav-link" href="/list">list</a>
287+            <a class="nav-link" href="/about">about</a>
288+            <a class="nav-link" href="/config">config</a>
289+          </div>
290         </nav>
291         <div class="container">
292             {%= p.Content() %}
293diff --git a/templates/base.qtpl.go b/templates/base.qtpl.go
294index 2573b99ef3b0fed08c2e52be745d2fe128a6ef07..1d30cd8e314963b6c91ca782b6d32948044283ee 100644
295--- a/templates/base.qtpl.go
296+++ b/templates/base.qtpl.go
297@@ -74,152 +74,153 @@         <meta name="viewport" content="width=device-width, initial-scale=1" />
298     </head>
299     <body>
300         <nav class="container navbar navbar-expand-sm">
301-          <ul class="navbar-nav">
302-            <li class="nav-link" href="/git">git</li>
303-            <li class="nav-link" href="/list">list</li>
304-            <li class="nav-link" href="/about">about</li>
305-          </ul>
306+          <div class="navbar-nav">
307+            <a class="nav-link" href="/git">git</a>
308+            <a class="nav-link" href="/list">list</a>
309+            <a class="nav-link" href="/about">about</a>
310+            <a class="nav-link" href="/config">config</a>
311+          </div>
312         </nav>
313         <div class="container">
314             `)
315-//line base.qtpl:42
316+//line base.qtpl:43
317 	p.StreamContent(qw422016)
318-//line base.qtpl:42
319+//line base.qtpl:43
320 	qw422016.N().S(`
321         </div>
322     </body>
323     `)
324-//line base.qtpl:45
325+//line base.qtpl:46
326 	p.StreamScript(qw422016)
327-//line base.qtpl:45
328+//line base.qtpl:46
329 	qw422016.N().S(`
330 </html>
331 `)
332-//line base.qtpl:47
333+//line base.qtpl:48
334 }
335 
336-//line base.qtpl:47
337+//line base.qtpl:48
338 func WritePageTemplate(qq422016 qtio422016.Writer, p Page) {
339-//line base.qtpl:47
340+//line base.qtpl:48
341 	qw422016 := qt422016.AcquireWriter(qq422016)
342-//line base.qtpl:47
343+//line base.qtpl:48
344 	StreamPageTemplate(qw422016, p)
345-//line base.qtpl:47
346+//line base.qtpl:48
347 	qt422016.ReleaseWriter(qw422016)
348-//line base.qtpl:47
349+//line base.qtpl:48
350 }
351 
352-//line base.qtpl:47
353+//line base.qtpl:48
354 func PageTemplate(p Page) string {
355-//line base.qtpl:47
356+//line base.qtpl:48
357 	qb422016 := qt422016.AcquireByteBuffer()
358-//line base.qtpl:47
359+//line base.qtpl:48
360 	WritePageTemplate(qb422016, p)
361-//line base.qtpl:47
362+//line base.qtpl:48
363 	qs422016 := string(qb422016.B)
364-//line base.qtpl:47
365+//line base.qtpl:48
366 	qt422016.ReleaseByteBuffer(qb422016)
367-//line base.qtpl:47
368+//line base.qtpl:48
369 	return qs422016
370-//line base.qtpl:47
371+//line base.qtpl:48
372 }
373 
374-//line base.qtpl:49
375+//line base.qtpl:50
376 type BasePage struct{}
377 
378-//line base.qtpl:50
379+//line base.qtpl:51
380 func (p *BasePage) StreamTitle(qw422016 *qt422016.Writer) {
381-//line base.qtpl:50
382+//line base.qtpl:51
383 	qw422016.N().S(`Empty`)
384-//line base.qtpl:50
385+//line base.qtpl:51
386 }
387 
388-//line base.qtpl:50
389+//line base.qtpl:51
390 func (p *BasePage) WriteTitle(qq422016 qtio422016.Writer) {
391-//line base.qtpl:50
392+//line base.qtpl:51
393 	qw422016 := qt422016.AcquireWriter(qq422016)
394-//line base.qtpl:50
395+//line base.qtpl:51
396 	p.StreamTitle(qw422016)
397-//line base.qtpl:50
398+//line base.qtpl:51
399 	qt422016.ReleaseWriter(qw422016)
400-//line base.qtpl:50
401+//line base.qtpl:51
402 }
403 
404-//line base.qtpl:50
405+//line base.qtpl:51
406 func (p *BasePage) Title() string {
407-//line base.qtpl:50
408+//line base.qtpl:51
409 	qb422016 := qt422016.AcquireByteBuffer()
410-//line base.qtpl:50
411+//line base.qtpl:51
412 	p.WriteTitle(qb422016)
413-//line base.qtpl:50
414+//line base.qtpl:51
415 	qs422016 := string(qb422016.B)
416-//line base.qtpl:50
417+//line base.qtpl:51
418 	qt422016.ReleaseByteBuffer(qb422016)
419-//line base.qtpl:50
420+//line base.qtpl:51
421 	return qs422016
422-//line base.qtpl:50
423+//line base.qtpl:51
424 }
425 
426-//line base.qtpl:51
427+//line base.qtpl:52
428 func (p *BasePage) StreamBody(qw422016 *qt422016.Writer) {
429-//line base.qtpl:51
430+//line base.qtpl:52
431 	qw422016.N().S(`HelloWorld`)
432-//line base.qtpl:51
433+//line base.qtpl:52
434 }
435 
436-//line base.qtpl:51
437+//line base.qtpl:52
438 func (p *BasePage) WriteBody(qq422016 qtio422016.Writer) {
439-//line base.qtpl:51
440+//line base.qtpl:52
441 	qw422016 := qt422016.AcquireWriter(qq422016)
442-//line base.qtpl:51
443+//line base.qtpl:52
444 	p.StreamBody(qw422016)
445-//line base.qtpl:51
446+//line base.qtpl:52
447 	qt422016.ReleaseWriter(qw422016)
448-//line base.qtpl:51
449+//line base.qtpl:52
450 }
451 
452-//line base.qtpl:51
453+//line base.qtpl:52
454 func (p *BasePage) Body() string {
455-//line base.qtpl:51
456+//line base.qtpl:52
457 	qb422016 := qt422016.AcquireByteBuffer()
458-//line base.qtpl:51
459+//line base.qtpl:52
460 	p.WriteBody(qb422016)
461-//line base.qtpl:51
462+//line base.qtpl:52
463 	qs422016 := string(qb422016.B)
464-//line base.qtpl:51
465+//line base.qtpl:52
466 	qt422016.ReleaseByteBuffer(qb422016)
467-//line base.qtpl:51
468+//line base.qtpl:52
469 	return qs422016
470-//line base.qtpl:51
471+//line base.qtpl:52
472 }
473 
474-//line base.qtpl:52
475+//line base.qtpl:53
476 func (p *BasePage) StreamScript(qw422016 *qt422016.Writer) {
477-//line base.qtpl:52
478+//line base.qtpl:53
479 }
480 
481-//line base.qtpl:52
482+//line base.qtpl:53
483 func (p *BasePage) WriteScript(qq422016 qtio422016.Writer) {
484-//line base.qtpl:52
485+//line base.qtpl:53
486 	qw422016 := qt422016.AcquireWriter(qq422016)
487-//line base.qtpl:52
488+//line base.qtpl:53
489 	p.StreamScript(qw422016)
490-//line base.qtpl:52
491+//line base.qtpl:53
492 	qt422016.ReleaseWriter(qw422016)
493-//line base.qtpl:52
494+//line base.qtpl:53
495 }
496 
497-//line base.qtpl:52
498+//line base.qtpl:53
499 func (p *BasePage) Script() string {
500-//line base.qtpl:52
501+//line base.qtpl:53
502 	qb422016 := qt422016.AcquireByteBuffer()
503-//line base.qtpl:52
504+//line base.qtpl:53
505 	p.WriteScript(qb422016)
506-//line base.qtpl:52
507+//line base.qtpl:53
508 	qs422016 := string(qb422016.B)
509-//line base.qtpl:52
510+//line base.qtpl:53
511 	qt422016.ReleaseByteBuffer(qb422016)
512-//line base.qtpl:52
513+//line base.qtpl:53
514 	return qs422016
515-//line base.qtpl:52
516+//line base.qtpl:53
517 }
518diff --git a/templates/helloworld.qtpl b/templates/helloworld.qtpl
519index 45c5595820b5fc751efd9f83275cb86da2d11ddc..1785bb5b399087bb3237a3673a9b1e3717e45e81 100644
520--- a/templates/helloworld.qtpl
521+++ b/templates/helloworld.qtpl
522@@ -1,13 +1,13 @@
523 {% code
524 type HelloPage struct {
525-    Body string
526+    Body []byte
527 }
528 %}
529 
530 {% func (p *HelloPage) Title() %}Hello{% endfunc %}
531 
532 {% func (p *HelloPage) Content() %}
533-{%s= p.Body %}
534+{%z= p.Body %}
535 {% endfunc %}
536 
537 {% func (p *HelloPage) Script() %}
538diff --git a/templates/helloworld.qtpl.go b/templates/helloworld.qtpl.go
539index 61c8f75a74d772c57fd5124d9966cfdbcba1f45c..7e37f3bca4276829888fe900fa3d9bf53ff094ed 100644
540--- a/templates/helloworld.qtpl.go
541+++ b/templates/helloworld.qtpl.go
542@@ -19,7 +19,7 @@ )
543 
544 //line helloworld.qtpl:2
545 type HelloPage struct {
546-	Body string
547+	Body []byte
548 }
549 
550 //line helloworld.qtpl:7
551@@ -61,7 +61,7 @@ //line helloworld.qtpl:9
552 	qw422016.N().S(`
553 `)
554 //line helloworld.qtpl:10
555-	qw422016.N().S(p.Body)
556+	qw422016.N().Z(p.Body)
557 //line helloworld.qtpl:10
558 	qw422016.N().S(`
559 `)