Skip to content

Commit 853a7a9

Browse files
retlehsclaude
andcommitted
Migrate templates from html/template to Jet v6
Replace Go's html/template with CloudyKit/jet/v6 for server-rendered templates. Jet provides first-class parameterized blocks, template inheritance via extends/yield, and C-like expressions — eliminating the dict helper workarounds that html/template required for reusable components. Add components.html with four reusable Jet blocks: - shell(command, variant, class) — copyable command boxes (replaces ~16 duplicated instances across 6 templates) - notice(variant, class) with yield content — alert boxes with variant-based styling and content slots - input(icon, name, ...) — form inputs with icon and keyboard hint - pagination(pager, class) — pagination controls driven by pre-computed buildPagination() data Key changes: - templates.go: embed.FS loader for Jet, function registration via AddGlobal, buildPagination() helper, render() using VarMap - handlers.go: simplified render calls (no more template combinations) - router.go: *jet.Set replaces *templateSet - All 13 templates converted to Jet syntax (extends/block/yield) - Tailwind scanning unaffected (templates remain .html files) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4d951d3 commit 853a7a9

19 files changed

Lines changed: 500 additions & 443 deletions

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ require (
2121
)
2222

2323
require (
24+
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
25+
github.com/CloudyKit/jet/v6 v6.3.2 // indirect
2426
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
2527
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
2628
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
2+
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
3+
github.com/CloudyKit/jet/v6 v6.3.2 h1:BPaX0lnXTZ9TniICiiK/0iJqzeGJ2ibvB4DjAqLMBSM=
4+
github.com/CloudyKit/jet/v6 v6.3.2/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
15
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
26
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
37
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY=

internal/http/handlers.go

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"encoding/json"
99
"fmt"
1010
"net/http"
11+
12+
"github.com/CloudyKit/jet/v6"
1113
"os"
1214
"path/filepath"
1315
"slices"
@@ -80,7 +82,7 @@ type versionRow struct {
8082
IsLatest bool
8183
}
8284

83-
func handleIndex(a *app.App, tmpl *templateSet) http.HandlerFunc {
85+
func handleIndex(a *app.App, tmpl *jet.Set) http.HandlerFunc {
8486
return func(w http.ResponseWriter, r *http.Request) {
8587
filters := publicFilters{
8688
Search: r.URL.Query().Get("search"),
@@ -126,23 +128,27 @@ func handleIndex(a *app.App, tmpl *templateSet) http.HandlerFunc {
126128
return
127129
}
128130

129-
render(w, r, tmpl.index, "layout", map[string]any{
131+
render(w, r, tmpl, "index.html", map[string]any{
130132
"Packages": packages,
131133
"Filters": filters,
132134
"Page": page,
133135
"Total": total,
134136
"TotalPages": totalPages,
135-
"Stats": stats,
136-
"AppURL": a.Config.AppURL,
137-
"CDNURL": a.Config.R2.CDNPublicURL,
138-
"OGImage": ogImageURL(a.Config, "social/default.png"),
139-
"JSONLD": jsonLDData,
140-
"BlogPosts": a.Blog.Posts(),
137+
"Pagination": buildPagination(page, totalPages, "#package-results", "#filter-form:top",
138+
func(p int) string { return paginateURL(filters, p) },
139+
func(p int) string { return paginatePartialURL(filters, p) },
140+
),
141+
"Stats": stats,
142+
"AppURL": a.Config.AppURL,
143+
"CDNURL": a.Config.R2.CDNPublicURL,
144+
"OGImage": ogImageURL(a.Config, "social/default.png"),
145+
"JSONLD": jsonLDData,
146+
"BlogPosts": a.Blog.Posts(),
141147
})
142148
}
143149
}
144150

145-
func handleIndexPartial(a *app.App, tmpl *templateSet) http.HandlerFunc {
151+
func handleIndexPartial(a *app.App, tmpl *jet.Set) http.HandlerFunc {
146152
return func(w http.ResponseWriter, r *http.Request) {
147153
filters := publicFilters{
148154
Search: r.URL.Query().Get("search"),
@@ -168,31 +174,35 @@ func handleIndexPartial(a *app.App, tmpl *templateSet) http.HandlerFunc {
168174
totalPages := (total + perPage - 1) / perPage
169175

170176
w.Header().Set("X-Robots-Tag", "noindex")
171-
render(w, r, tmpl.indexPartial, "package-results", map[string]any{
177+
render(w, r, tmpl, "package_results.html", map[string]any{
172178
"Packages": packages,
173179
"Filters": filters,
174180
"Page": page,
175181
"Total": total,
176182
"TotalPages": totalPages,
183+
"Pagination": buildPagination(page, totalPages, "#package-results", "#filter-form:top",
184+
func(p int) string { return paginateURL(filters, p) },
185+
func(p int) string { return paginatePartialURL(filters, p) },
186+
),
177187
})
178188
}
179189
}
180190

181-
func handleDocs(a *app.App, tmpl *templateSet) http.HandlerFunc {
191+
func handleDocs(a *app.App, tmpl *jet.Set) http.HandlerFunc {
182192
return func(w http.ResponseWriter, r *http.Request) {
183193
w.Header().Set("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400")
184-
render(w, r, tmpl.docs, "layout", map[string]any{
194+
render(w, r, tmpl, "docs.html", map[string]any{
185195
"AppURL": a.Config.AppURL,
186196
"CDNURL": a.Config.R2.CDNPublicURL,
187197
"OGImage": ogImageURL(a.Config, "social/default.png"),
188198
})
189199
}
190200
}
191201

192-
func handleCompare(a *app.App, tmpl *templateSet) http.HandlerFunc {
202+
func handleCompare(a *app.App, tmpl *jet.Set) http.HandlerFunc {
193203
return func(w http.ResponseWriter, r *http.Request) {
194204
w.Header().Set("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400")
195-
render(w, r, tmpl.compare, "layout", map[string]any{
205+
render(w, r, tmpl, "compare.html", map[string]any{
196206
"AppURL": a.Config.AppURL,
197207
"CDNURL": a.Config.R2.CDNPublicURL,
198208
"OGImage": ogImageURL(a.Config, "social/default.png"),
@@ -202,7 +212,7 @@ func handleCompare(a *app.App, tmpl *templateSet) http.HandlerFunc {
202212

203213
const untaggedPerPage = 20
204214

205-
func handleUntagged(a *app.App, tmpl *templateSet) http.HandlerFunc {
215+
func handleUntagged(a *app.App, tmpl *jet.Set) http.HandlerFunc {
206216
return func(w http.ResponseWriter, r *http.Request) {
207217
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
208218
if page < 1 {
@@ -227,7 +237,7 @@ func handleUntagged(a *app.App, tmpl *templateSet) http.HandlerFunc {
227237
_ = a.DB.QueryRowContext(r.Context(), "SELECT active_plugins FROM package_stats WHERE id = 1").Scan(&totalPlugins)
228238

229239
w.Header().Set("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400")
230-
render(w, r, tmpl.untagged, "layout", map[string]any{
240+
render(w, r, tmpl, "untagged.html", map[string]any{
231241
"Packages": packages,
232242
"Filter": filter,
233243
"Search": search,
@@ -237,14 +247,18 @@ func handleUntagged(a *app.App, tmpl *templateSet) http.HandlerFunc {
237247
"Total": int64(total),
238248
"TotalPlugins": totalPlugins,
239249
"TotalPages": totalPages,
240-
"AppURL": a.Config.AppURL,
241-
"CDNURL": a.Config.R2.CDNPublicURL,
242-
"OGImage": ogImageURL(a.Config, "social/default.png"),
250+
"Pagination": buildPagination(page, totalPages, "#untagged-results", "#untagged-form:top",
251+
func(p int) string { return untaggedPaginateURL(filter, search, author, sort, p) },
252+
func(p int) string { return untaggedPaginatePartialURL(filter, search, author, sort, p) },
253+
),
254+
"AppURL": a.Config.AppURL,
255+
"CDNURL": a.Config.R2.CDNPublicURL,
256+
"OGImage": ogImageURL(a.Config, "social/default.png"),
243257
})
244258
}
245259
}
246260

247-
func handleUntaggedPartial(a *app.App, tmpl *templateSet) http.HandlerFunc {
261+
func handleUntaggedPartial(a *app.App, tmpl *jet.Set) http.HandlerFunc {
248262
return func(w http.ResponseWriter, r *http.Request) {
249263
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
250264
if page < 1 {
@@ -266,7 +280,7 @@ func handleUntaggedPartial(a *app.App, tmpl *templateSet) http.HandlerFunc {
266280
totalPages := (total + untaggedPerPage - 1) / untaggedPerPage
267281

268282
w.Header().Set("X-Robots-Tag", "noindex")
269-
render(w, r, tmpl.untaggedPartial, "untagged-results", map[string]any{
283+
render(w, r, tmpl, "untagged_results.html", map[string]any{
270284
"Packages": packages,
271285
"Filter": filter,
272286
"Search": search,
@@ -275,6 +289,10 @@ func handleUntaggedPartial(a *app.App, tmpl *templateSet) http.HandlerFunc {
275289
"Page": page,
276290
"Total": int64(total),
277291
"TotalPages": totalPages,
292+
"Pagination": buildPagination(page, totalPages, "#untagged-results", "#untagged-form:top",
293+
func(p int) string { return untaggedPaginateURL(filter, search, author, sort, p) },
294+
func(p int) string { return untaggedPaginatePartialURL(filter, search, author, sort, p) },
295+
),
278296
})
279297
}
280298
}
@@ -324,10 +342,10 @@ func handleUntaggedAuthors(a *app.App) http.HandlerFunc {
324342
}
325343
}
326344

327-
func handleWordpressCore(a *app.App, tmpl *templateSet) http.HandlerFunc {
345+
func handleWordpressCore(a *app.App, tmpl *jet.Set) http.HandlerFunc {
328346
return func(w http.ResponseWriter, r *http.Request) {
329347
w.Header().Set("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400")
330-
render(w, r, tmpl.wordpressCore, "layout", map[string]any{
348+
render(w, r, tmpl, "wordpress_core.html", map[string]any{
331349
"AppURL": a.Config.AppURL,
332350
"CDNURL": a.Config.R2.CDNPublicURL,
333351
"OGImage": ogImageURL(a.Config, "social/default.png"),
@@ -336,7 +354,7 @@ func handleWordpressCore(a *app.App, tmpl *templateSet) http.HandlerFunc {
336354
}
337355
}
338356

339-
func handleDetail(a *app.App, tmpl *templateSet) http.HandlerFunc {
357+
func handleDetail(a *app.App, tmpl *jet.Set) http.HandlerFunc {
340358
return func(w http.ResponseWriter, r *http.Request) {
341359
pkgType := r.PathValue("type")
342360
name := r.PathValue("name")
@@ -351,7 +369,7 @@ func handleDetail(a *app.App, tmpl *templateSet) http.HandlerFunc {
351369
http.Redirect(w, r, "https://wp-packages.org/", http.StatusFound)
352370
} else {
353371
w.WriteHeader(http.StatusNotFound)
354-
render(w, r, tmpl.notFound, "layout", map[string]any{"Gone": false, "CDNURL": a.Config.R2.CDNPublicURL})
372+
render(w, r, tmpl, "404.html", map[string]any{"Gone": false, "CDNURL": a.Config.R2.CDNPublicURL})
355373
}
356374
return
357375
}
@@ -429,7 +447,7 @@ func handleDetail(a *app.App, tmpl *templateSet) http.HandlerFunc {
429447
return
430448
}
431449

432-
render(w, r, tmpl.detail, "layout", map[string]any{
450+
render(w, r, tmpl, "detail.html", map[string]any{
433451
"Package": pkg,
434452
"Versions": versions,
435453
"MonthlyInstalls": monthlyInstalls,
@@ -449,9 +467,9 @@ var logFiles = map[string]string{
449467
"check-status": filepath.Join("storage", "logs", "check-status.log"),
450468
}
451469

452-
func handleAdminLogs(tmpl *templateSet) http.HandlerFunc {
470+
func handleAdminLogs(tmpl *jet.Set) http.HandlerFunc {
453471
return func(w http.ResponseWriter, r *http.Request) {
454-
render(w, r, tmpl.adminLogs, "admin_layout", nil)
472+
render(w, r, tmpl, "admin_logs.html", nil)
455473
}
456474
}
457475

@@ -937,7 +955,7 @@ type statusPageCheck struct {
937955
Changes []packages.StatusCheckChange
938956
}
939957

940-
func handleStatus(a *app.App, tmpl *templateSet) http.HandlerFunc {
958+
func handleStatus(a *app.App, tmpl *jet.Set) http.HandlerFunc {
941959
return func(w http.ResponseWriter, r *http.Request) {
942960
ctx := r.Context()
943961
cutoff := time.Now().Add(-24 * time.Hour).UnixMilli()
@@ -1022,10 +1040,12 @@ func handleStatus(a *app.App, tmpl *templateSet) http.HandlerFunc {
10221040
"PackagesUpdated24h": packagesUpdated24h,
10231041
"Deactivated24h": deactivated24h,
10241042
"Reactivated24h": reactivated24h,
1043+
"AppURL": a.Config.AppURL,
1044+
"CDNURL": a.Config.R2.CDNPublicURL,
10251045
}
10261046
if len(statusBuilds) > 0 {
10271047
data["LastBuildStartedAt"] = statusBuilds[0].StartedAt
10281048
}
1029-
render(w, r, tmpl.status, "layout", data)
1049+
render(w, r, tmpl, "status.html", data)
10301050
}
10311051
}

internal/http/router.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"time"
1313

14+
"github.com/CloudyKit/jet/v6"
1415
sentryhttp "github.com/getsentry/sentry-go/http"
1516
"github.com/roots/wp-packages/internal/app"
1617
)
@@ -146,7 +147,7 @@ func NewRouter(a *app.App) http.Handler {
146147
// handler touched the response (checked via a context flag set by routeMarker),
147148
// replace the default body with the custom template. 405s and handler-generated
148149
// 404s pass through untouched.
149-
func appHandler(mux *http.ServeMux, tmpl *templateSet, a *app.App, sitemapPackages http.HandlerFunc) http.Handler {
150+
func appHandler(mux *http.ServeMux, tmpl *jet.Set, a *app.App, sitemapPackages http.HandlerFunc) http.Handler {
150151
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151152
// Sitemap-packages prefix can't be expressed as a ServeMux pattern
152153
// (wildcards must be full path segments).
@@ -163,7 +164,7 @@ func appHandler(mux *http.ServeMux, tmpl *templateSet, a *app.App, sitemapPackag
163164
// so rec.dispatched is false only when the mux itself returned 404/405/etc.
164165
if rec.code == http.StatusNotFound && !rec.dispatched {
165166
w.WriteHeader(http.StatusNotFound)
166-
render(w, r, tmpl.notFound, "layout", map[string]any{"Gone": false, "CDNURL": a.Config.R2.CDNPublicURL})
167+
render(w, r, tmpl, "404.html", map[string]any{"Gone": false, "CDNURL": a.Config.R2.CDNPublicURL})
167168
}
168169
})
169170
}

0 commit comments

Comments
 (0)