switch to Geist and cache help center pages

Geist replaces Instrument Sans and is served from static/ instead of Google
Fonts, so it works on installs with no internet access.

Public help center pages are cached in Redis via fastcache with ETags, and any
admin write clears the group so edits show up on the next load.
This commit is contained in:
Abhinav Raut
2026-08-20 18:01:42 +05:30
parent a0d9929ecc
commit c865748ca5
26 changed files with 500 additions and 61 deletions
+20 -20
View File
@@ -37,7 +37,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
// Settings.
g.GET("/api/v1/settings/general", auth(handleGetGeneralSettings))
g.PUT("/api/v1/settings/general", perm(handleUpdateGeneralSettings, "general_settings:manage"))
g.PUT("/api/v1/settings/general", perm(clearsHelpCenterCache(handleUpdateGeneralSettings), "general_settings:manage"))
g.GET("/api/v1/settings/notifications/email", perm(handleGetEmailNotificationSettings, "notification_settings:manage"))
g.PUT("/api/v1/settings/notifications/email", perm(handleUpdateEmailNotificationSettings, "notification_settings:manage"))
@@ -295,24 +295,24 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
g.GET("/api/v1/help-centers/locales", perm(handleGetHelpCenterLocales, "help_center:manage"))
g.GET("/api/v1/help-centers/{id}", perm(handleGetHelpCenter, "help_center:manage"))
g.GET("/api/v1/help-centers/{id}/tree", perm(handleGetHelpCenterTree, "help_center:manage"))
g.POST("/api/v1/help-centers", perm(handleCreateHelpCenter, "help_center:manage"))
g.PUT("/api/v1/help-centers/{id}", perm(handleUpdateHelpCenter, "help_center:manage"))
g.POST("/api/v1/help-centers", perm(clearsHelpCenterCache(handleCreateHelpCenter), "help_center:manage"))
g.PUT("/api/v1/help-centers/{id}", perm(clearsHelpCenterCache(handleUpdateHelpCenter), "help_center:manage"))
g.POST("/api/v1/help-centers/{id}/preview", perm(handleHelpCenterPreview, "help_center:manage"))
g.PUT("/api/v1/help-centers/{id}/toggle", perm(handleToggleHelpCenterActive, "help_center:manage"))
g.DELETE("/api/v1/help-centers/{id}", perm(handleDeleteHelpCenter, "help_center:manage"))
g.PUT("/api/v1/help-centers/{id}/toggle", perm(clearsHelpCenterCache(handleToggleHelpCenterActive), "help_center:manage"))
g.DELETE("/api/v1/help-centers/{id}", perm(clearsHelpCenterCache(handleDeleteHelpCenter), "help_center:manage"))
g.GET("/api/v1/help-centers/{hc_id}/collections", perm(handleGetCollections, "help_center:manage"))
g.POST("/api/v1/help-centers/{hc_id}/collections", perm(handleCreateCollection, "help_center:manage"))
g.PUT("/api/v1/help-centers/{hc_id}/collections/{id}", perm(handleUpdateCollection, "help_center:manage"))
g.DELETE("/api/v1/help-centers/{hc_id}/collections/{id}", perm(handleDeleteCollection, "help_center:manage"))
g.PUT("/api/v1/help-centers/{hc_id}/collection-sort-order", perm(handleUpdateCollectionSortOrders, "help_center:manage"))
g.PUT("/api/v1/collections/{id}/toggle", perm(handleToggleCollection, "help_center:manage"))
g.PUT("/api/v1/collections/{col_id}/article-sort-order", perm(handleUpdateArticleSortOrders, "help_center:manage"))
g.POST("/api/v1/help-centers/{hc_id}/collections", perm(clearsHelpCenterCache(handleCreateCollection), "help_center:manage"))
g.PUT("/api/v1/help-centers/{hc_id}/collections/{id}", perm(clearsHelpCenterCache(handleUpdateCollection), "help_center:manage"))
g.DELETE("/api/v1/help-centers/{hc_id}/collections/{id}", perm(clearsHelpCenterCache(handleDeleteCollection), "help_center:manage"))
g.PUT("/api/v1/help-centers/{hc_id}/collection-sort-order", perm(clearsHelpCenterCache(handleUpdateCollectionSortOrders), "help_center:manage"))
g.PUT("/api/v1/collections/{id}/toggle", perm(clearsHelpCenterCache(handleToggleCollection), "help_center:manage"))
g.PUT("/api/v1/collections/{col_id}/article-sort-order", perm(clearsHelpCenterCache(handleUpdateArticleSortOrders), "help_center:manage"))
g.GET("/api/v1/collections/{col_id}/articles/{id}", perm(handleGetArticle, "help_center:manage"))
g.POST("/api/v1/collections/{col_id}/articles", perm(handleCreateArticle, "help_center:manage"))
g.DELETE("/api/v1/collections/{col_id}/articles/{id}", perm(handleDeleteArticle, "help_center:manage"))
g.PUT("/api/v1/articles/{id}", perm(handleUpdateArticle, "help_center:manage"))
g.PUT("/api/v1/articles/{id}/collection", perm(handleMoveArticle, "help_center:manage"))
g.PUT("/api/v1/articles/{id}/status", perm(handleUpdateArticleStatus, "help_center:manage"))
g.POST("/api/v1/collections/{col_id}/articles", perm(clearsHelpCenterCache(handleCreateArticle), "help_center:manage"))
g.DELETE("/api/v1/collections/{col_id}/articles/{id}", perm(clearsHelpCenterCache(handleDeleteArticle), "help_center:manage"))
g.PUT("/api/v1/articles/{id}", perm(clearsHelpCenterCache(handleUpdateArticle), "help_center:manage"))
g.PUT("/api/v1/articles/{id}/collection", perm(clearsHelpCenterCache(handleMoveArticle), "help_center:manage"))
g.PUT("/api/v1/articles/{id}/status", perm(clearsHelpCenterCache(handleUpdateArticleStatus), "help_center:manage"))
g.GET("/api/v1/help-centers/{id}/insights", perm(handleGetHelpCenterInsights, "help_center:manage"))
// Public help center JSON API.
@@ -393,11 +393,11 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
getAndHead("/robots.txt", rateLimit(handleRobotsTxt, "public"))
getAndHead("/sitemap.xml", rateLimit(handleSitemapIndex, "public"))
getAndHead("/hc/{slug}", rateLimit(handleRedirectHelpCenterHome, "public"))
getAndHead("/hc/{slug}/{locale}", rateLimit(handleShowHelpCenterHome, "public"))
getAndHead("/hc/{slug}/{locale}", rateLimit(cachedHelpCenterPage(handleShowHelpCenterHome), "public"))
getAndHead("/hc/{slug}/{locale}/sitemap.xml", rateLimit(handleHelpCenterSitemap, "public"))
getAndHead("/hc/{slug}/{locale}/search", rateLimit(handleHelpCenterSearch, "public"))
getAndHead("/hc/{slug}/{locale}/collections/{collection_slug}", rateLimit(handleShowHelpCenterCollection, "public"))
getAndHead("/hc/{slug}/{locale}/articles/{article_slug}", rateLimit(handleShowHelpCenterArticle, "public"))
getAndHead("/hc/{slug}/{locale}/search", rateLimit(cachedHelpCenterPage(handleHelpCenterSearch), "public"))
getAndHead("/hc/{slug}/{locale}/collections/{collection_slug}", rateLimit(cachedHelpCenterPage(handleShowHelpCenterCollection), "public"))
getAndHead("/hc/{slug}/{locale}/articles/{article_slug}", rateLimit(countArticleView(cachedHelpCenterPage(handleShowHelpCenterArticle)), "public"))
g.GET("/csat/{uuid}", rateLimit(handleShowCSAT, "public"))
g.GET("/csat/{uuid}/widget", rateLimit(handleShowCSATWidget, "public"))
+91 -7
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"hash/fnv"
"html/template"
"io"
"log"
"math"
"net"
@@ -25,6 +26,7 @@ import (
realip "github.com/ferluci/fast-realip"
"github.com/knadh/stuffbin"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastcache/v4"
"github.com/zerodha/fastglue"
)
@@ -43,7 +45,15 @@ const (
sitemapDate = "2006-01-02"
schemaOrgContext = "https://schema.org"
helpCenterCacheControl = "public, max-age=300, stale-while-revalidate=3600"
helpCenterCacheControl = "public, no-cache"
fastCachePrefix = "libredesk:cache:"
helpCenterCacheTTL = 24 * time.Hour
helpCenterCacheGroup = "helpcenter"
helpCenterXMLCacheControl = "public, max-age=300, stale-while-revalidate=3600"
// articleFeedbackDedupTTL is how long a reader IP's vote on an article suppresses further votes.
articleFeedbackDedupTTL = 24 * time.Hour
@@ -60,6 +70,16 @@ const (
)
var (
// Logger is set because fastcache writes to the field when it finds it nil, racing across requests.
helpCenterCacheOpts = &fastcache.Options{
NamespaceKey: "slug",
ETag: true,
IncludeQueryString: true,
// Backstop for keys nothing invalidates, such as a deleted help center's.
TTL: helpCenterCacheTTL,
Logger: log.New(io.Discard, "", 0),
}
// crawlerUARe matches search and preview bots, whose hits must not count as reader views.
crawlerUARe = regexp.MustCompile(`(?i)bot\b|bot/|crawler|spider|crawling|slurp|facebookexternalhit|preview|headlesschrome|lighthouse|feedfetcher|python-requests|curl/|wget/`)
@@ -689,9 +709,6 @@ func handleShowHelpCenterArticle(r *fastglue.Request) error {
}
// The JSON-LD embeds the author too, not just the byline.
hideArticleAuthor(helpCenterTheme(helpCenter), &article)
if !isCrawler(r) {
app.helpcenter.IncrementArticleViewCount(article.ID)
}
if markdown {
r.RequestCtx.Response.Header.Set("X-Robots-Tag", noIndexHeader)
@@ -1055,11 +1072,13 @@ func handleUpdateArticle(r *fastglue.Request) error {
// no-store default that RenderWebPage applies for the app's authenticated pages.
func renderHelpCenterPage(r *fastglue.Request, name string, data map[string]interface{}) error {
app := r.Context.(*App)
err := app.tmpl.RenderWebPage(r.RequestCtx, name, data)
if err := app.tmpl.RenderWebPage(r.RequestCtx, name, data); err != nil {
return err
}
r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl)
r.RequestCtx.Response.Header.Del("Pragma")
r.RequestCtx.Response.Header.Del("Expires")
return err
return nil
}
// sendXML writes v as an XML document.
@@ -1069,12 +1088,77 @@ func sendXML(r *fastglue.Request, v any) error {
return sendErrorEnvelope(r, err)
}
r.RequestCtx.SetContentType("application/xml; charset=utf-8")
r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl)
r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterXMLCacheControl)
fmt.Fprint(r.RequestCtx, xml.Header)
r.RequestCtx.Write(out)
return nil
}
// countArticleView records a view after the page is served, counting cache hits too.
func countArticleView(h fastglue.FastRequestHandler) fastglue.FastRequestHandler {
return func(r *fastglue.Request) error {
err := h(r)
switch r.RequestCtx.Response.StatusCode() {
case fasthttp.StatusOK, fasthttp.StatusNotModified:
if !isCrawler(r) {
app := r.Context.(*App)
slug, _ := r.RequestCtx.UserValue("slug").(string)
articleSlug, _ := r.RequestCtx.UserValue("article_slug").(string)
locale, _ := r.RequestCtx.UserValue("locale").(string)
app.helpcenter.IncrementPublishedArticleView(slug, strings.TrimSuffix(articleSlug, markdownSlugExtension), locale)
}
}
return err
}
}
// cachedHelpCenterPage serves a public help center page from the response cache, which
// stores the body alone, so the caching headers are re-applied on the hit and 304 paths.
func cachedHelpCenterPage(h fastglue.FastRequestHandler) fastglue.FastRequestHandler {
return func(r *fastglue.Request) error {
app := r.Context.(*App)
err := app.fastCache.Cached(h, helpCenterCacheOpts, helpCenterCacheGroup)(r)
switch r.RequestCtx.Response.StatusCode() {
case fasthttp.StatusOK, fasthttp.StatusNotModified:
r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl)
r.RequestCtx.Response.Header.Del("Pragma")
r.RequestCtx.Response.Header.Del("Expires")
}
return err
}
}
// clearsHelpCenterCache drops the cached public pages after a successful write.
func clearsHelpCenterCache(h fastglue.FastRequestHandler) fastglue.FastRequestHandler {
return func(r *fastglue.Request) error {
err := h(r)
if r.RequestCtx.Response.StatusCode() < fasthttp.StatusMultipleChoices {
clearHelpCenterCache(r.Context.(*App), "")
}
return err
}
}
// clearHelpCenterCache drops every cached public page for slug, or for all when slug is empty.
func clearHelpCenterCache(app *App, slug string) {
if slug != "" {
if err := app.fastCache.DelGroup(slug, helpCenterCacheGroup); err != nil {
app.lo.Error("error clearing help center cache", "slug", slug, "error", err)
}
return
}
helpCenters, err := app.helpcenter.GetAllHelpCenters()
if err != nil {
app.lo.Error("error listing help centers to clear cache", "error", err)
return
}
for _, hc := range helpCenters {
if err := app.fastCache.DelGroup(hc.Slug, helpCenterCacheGroup); err != nil {
app.lo.Error("error clearing help center cache", "slug", hc.Slug, "error", err)
}
}
}
// isCrawler reports whether the request came from a bot rather than a reader; HEAD probes never count as readers.
func isCrawler(r *fastglue.Request) bool {
return r.RequestCtx.IsHead() || crawlerUARe.Match(r.RequestCtx.Request.Header.UserAgent())
+138
View File
@@ -0,0 +1,138 @@
package main
import (
"testing"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/valyala/fasthttp"
goredis "github.com/zerodha/fastcache/stores/goredis/v9"
"github.com/zerodha/fastcache/v4"
"github.com/zerodha/fastglue"
)
func TestHelpCenterPageCacheServesAndInvalidates(t *testing.T) {
fc := newTestFastCache(t)
renders := 0
body := "<h1>original</h1>"
handler := func(r *fastglue.Request) error {
renders++
r.RequestCtx.SetContentType("text/html; charset=utf-8")
r.RequestCtx.Response.Header.Set("Cache-Control", helpCenterCacheControl)
r.RequestCtx.Response.SetBodyString(body)
return nil
}
cached := fc.Cached(handler, helpCenterCacheOpts, helpCenterCacheGroup)
const uri = "/hc/support/en/articles/refunds"
first := request(uri, "support", "")
if err := cached(first); err != nil {
t.Fatalf("first request: %v", err)
}
if renders != 1 {
t.Fatalf("first request should render, renders = %d", renders)
}
etag := string(first.RequestCtx.Response.Header.Peek("ETag"))
if etag == "" {
t.Fatal("expected an ETag on the rendered page")
}
second := request(uri, "support", "")
if err := cached(second); err != nil {
t.Fatalf("second request: %v", err)
}
if renders != 1 {
t.Errorf("second request re-rendered instead of using the cache, renders = %d", renders)
}
if got := string(second.RequestCtx.Response.Body()); got != body {
t.Errorf("cached body = %q, want %q", got, body)
}
revalidate := request(uri, "support", etag)
if err := cached(revalidate); err != nil {
t.Fatalf("revalidation: %v", err)
}
if got := revalidate.RequestCtx.Response.StatusCode(); got != fasthttp.StatusNotModified {
t.Errorf("matching ETag returned %d, want 304", got)
}
if got := revalidate.RequestCtx.Response.Body(); len(got) != 0 {
t.Errorf("304 carried a body of %d bytes", len(got))
}
// An admin write clears the group, so the next read must re-render the edit.
if err := fc.DelGroup("support", helpCenterCacheGroup); err != nil {
t.Fatalf("clearing the group: %v", err)
}
body = "<h1>edited</h1>"
afterEdit := request(uri, "support", "")
if err := cached(afterEdit); err != nil {
t.Fatalf("request after edit: %v", err)
}
if renders != 2 {
t.Errorf("cleared cache did not re-render, renders = %d", renders)
}
if got := string(afterEdit.RequestCtx.Response.Body()); got != body {
t.Errorf("stale body served after an edit: got %q, want %q", got, body)
}
if newETag := string(afterEdit.RequestCtx.Response.Header.Peek("ETag")); newETag == etag {
t.Error("ETag survived an edit, readers holding the old copy would never refetch")
}
}
func TestHelpCenterPageCacheIsolatesHelpCentersAndQueries(t *testing.T) {
fc := newTestFastCache(t)
served := map[string]int{}
handler := func(r *fastglue.Request) error {
uri := string(r.RequestCtx.URI().RequestURI())
served[uri]++
r.RequestCtx.Response.SetBodyString(uri)
return nil
}
cached := fc.Cached(handler, helpCenterCacheOpts, helpCenterCacheGroup)
for _, req := range []*fastglue.Request{
request("/hc/support/en/search?q=refund", "support", ""),
request("/hc/support/en/search?q=billing", "support", ""),
request("/hc/docs/en/search?q=refund", "docs", ""),
} {
if err := cached(req); err != nil {
t.Fatalf("request: %v", err)
}
}
if len(served) != 3 {
t.Fatalf("expected 3 distinct cache entries, got %d: %v", len(served), served)
}
// Clearing one help center must not touch another's pages.
if err := fc.DelGroup("support", helpCenterCacheGroup); err != nil {
t.Fatalf("clearing the group: %v", err)
}
if err := cached(request("/hc/docs/en/search?q=refund", "docs", "")); err != nil {
t.Fatalf("request: %v", err)
}
if got := served["/hc/docs/en/search?q=refund"]; got != 1 {
t.Errorf("clearing 'support' evicted 'docs', renders = %d", got)
}
}
// newTestFastCache returns a cache backed by an in-memory redis.
func newTestFastCache(t *testing.T) *fastcache.FastCache {
t.Helper()
mr := miniredis.RunT(t)
return fastcache.New(goredis.New(goredis.Config{Prefix: fastCachePrefix}, redis.NewClient(&redis.Options{Addr: mr.Addr()})))
}
// request builds a GET for the given URI with the slug namespace the options read.
func request(uri, slug, ifNoneMatch string) *fastglue.Request {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod("GET")
ctx.Request.SetRequestURI(uri)
ctx.SetUserValue("slug", slug)
if ifNoneMatch != "" {
ctx.Request.Header.Set("If-None-Match", ifNoneMatch)
}
return &fastglue.Request{RequestCtx: ctx}
}
+7
View File
@@ -70,6 +70,8 @@ import (
"github.com/redis/go-redis/v9"
flag "github.com/spf13/pflag"
"github.com/volatiletech/null/v9"
goredis "github.com/zerodha/fastcache/stores/goredis/v9"
"github.com/zerodha/fastcache/v4"
"github.com/zerodha/logf"
)
@@ -940,6 +942,11 @@ func initRedis() *redis.Client {
})
}
// initFastCache inits the shared HTTP response cache.
func initFastCache(rdb *redis.Client) *fastcache.FastCache {
return fastcache.New(goredis.New(goredis.Config{Prefix: fastCachePrefix}, rdb))
}
// initRedis inits postgres DB.
func initDB() *sqlx.DB {
dsn := fmt.Sprintf(
+3
View File
@@ -57,6 +57,7 @@ import (
"github.com/knadh/koanf/v2"
"github.com/knadh/stuffbin"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastcache/v4"
"github.com/zerodha/fastglue"
"github.com/zerodha/logf"
)
@@ -128,6 +129,7 @@ type App struct {
contextLink *contextlink.Manager
rateLimit *ratelimit.Limiter
redis *redis.Client
fastCache *fastcache.FastCache
importer *importer.Importer
wsHub *ws.Hub
@@ -322,6 +324,7 @@ func main() {
contextLink: initContextLink(db, i18n),
rateLimit: rateLimiter,
redis: rdb,
fastCache: initFastCache(rdb),
userNotification: userNotification,
wsHub: wsHub,
}
+1 -1
View File
@@ -92,7 +92,7 @@ In light mode `card` is pure white against a near-white background. In dark mode
## 3. Typography
Font: Instrument Sans. Sizes: `text-xs` 12 · `text-sm` 14 · `text-base` 16 · `text-lg` 18 ·
Font: Geist. Sizes: `text-xs` 12 · `text-sm` 14 · `text-base` 16 · `text-lg` 18 ·
`text-xl` 20 · `text-2xl` 24 · `text-3xl` 30. Weights: 400 body · 500 labels · 600 headings ·
700 rare emphasis.
+2 -4
View File
@@ -5,10 +5,8 @@
<meta charset="UTF-8" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&display=swap"
rel="stylesheet">
<link rel="preload" href="/static/public/static/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/static/public/static/fonts.css">
</head>
<body>
@@ -18,7 +18,7 @@
<div>
<SelectComboBox
v-model="conversationStore.current.assigned_user_id"
:items="[{ value: 'none', label: t('globals.terms.none') }, ...usersStore.options]"
:items="agentOptions"
:placeholder="t('placeholders.selectAgent')"
@select="selectAgent"
type="user"
@@ -264,6 +264,14 @@ const applySuggestedTag = (tag) => {
const priorityOptions = computed(() => conversationStore.priorityOptions)
const agentOptions = computed(() => {
const none = { value: 'none', label: t('globals.terms.none') }
const isMe = (option) => String(option.value) === String(userStore.userID)
const me = usersStore.options.find(isMe)
if (!me) return [none, ...usersStore.options]
return [none, me, ...usersStore.options.filter((option) => !isMe(option))]
})
const fetchTags = async () => {
await tagStore.fetchTags()
tags.value = tagStore.tags.map((item) => item.name)
+2 -4
View File
@@ -6,10 +6,8 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>libredesk Chat Widget</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&display=swap"
rel="stylesheet">
<link rel="preload" href="/static/public/static/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/static/public/static/fonts.css">
</head>
<body>
+1 -1
View File
@@ -22,7 +22,7 @@
@layer base {
html,
body {
font-family: 'Instrument Sans', sans-serif;
@apply font-sans;
min-height: 100%;
overflow: hidden;
margin: 0;
+4
View File
@@ -1,6 +1,7 @@
const animate = require("tailwindcss-animate")
const typography = require("@tailwindcss/typography")
const plugin = require("tailwindcss/plugin")
const defaultTheme = require("tailwindcss/defaultTheme")
const canHover = plugin(({ addVariant }) => addVariant("can-hover", "@media (hover: hover)"))
@@ -26,6 +27,9 @@ module.exports = {
}
},
extend: {
fontFamily: {
sans: ['Geist', ...defaultTheme.fontFamily.sans]
},
height: {
screen: '100dvh'
},
+3 -1
View File
@@ -4,7 +4,7 @@ go 1.25.0
require (
github.com/abhinavxd/ssrfguard v0.1.0
github.com/alicebob/miniredis/v2 v2.32.1
github.com/alicebob/miniredis/v2 v2.33.0
github.com/coreos/go-oidc/v3 v3.11.0
github.com/disintegration/imaging v1.6.2
github.com/emersion/go-imap/v2 v2.0.0-beta.3
@@ -42,6 +42,8 @@ require (
github.com/valyala/fasthttp v1.62.0
github.com/volatiletech/null/v9 v9.0.0
github.com/yuin/goldmark v1.8.4
github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0
github.com/zerodha/fastcache/v4 v4.2.1
github.com/zerodha/fastglue v1.8.0
github.com/zerodha/logf v0.5.5
github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0
+84 -7
View File
@@ -1,14 +1,19 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/abhinavxd/ssrfguard v0.1.0 h1:Ns/llAQ63uGFehxSvhCd+WGDKmBEEmIH+E1AW1CGgWM=
github.com/abhinavxd/ssrfguard v0.1.0/go.mod h1:eNVubb+m/r3KrKWYdG6hxzeAfj+t2ZmZss4V/x7D6Ws=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.32.1 h1:Bz7CciDnYSaa0mX5xODh6GUITRSx+cVhjNoOR4JssBo=
github.com/alicebob/miniredis/v2 v2.32.1/go.mod h1:AqkLNAfUm0K07J28hnAyyQKf/x0YkCY/g5DCtuL01Mw=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
@@ -19,19 +24,26 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -39,8 +51,16 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/emersion/go-imap/v2 v2.0.0-beta.3 h1:z0TLMfYnDsFupXLhzRXgOzXenD3uPvNniQSu5fN1teg=
github.com/emersion/go-imap/v2 v2.0.0-beta.3/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E=
@@ -54,6 +74,8 @@ github.com/fasthttp/websocket v1.5.9 h1:9deGuzYcCRKjk940kNwSN6Hd14hk4zYwropm4UsU
github.com/fasthttp/websocket v1.5.9/go.mod h1:NLzHBFur260OMuZHohOfYQwMTpR7sfSpUnuqKxMpgKA=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/ferluci/fast-realip v1.0.1 h1:zPi0iv7zgOOlM/qJt9mozLz5IjVRAezaqHJoQ0JJ/yI=
github.com/ferluci/fast-realip v1.0.1/go.mod h1:Ag7xdRQ9GOCL/pwbDe4zJv6SlfYdROArc8O+qIKhRc4=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -62,6 +84,12 @@ github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -71,6 +99,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
@@ -131,8 +161,12 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mackee/go-readability v0.3.1 h1:DUwcwlhNLPtrBkGyJPcKp51oOKBvZvvMDPPFFLUIcKc=
github.com/mackee/go-readability v0.3.1/go.mod h1:lfyLr0PJ+fQ+z6r6IBrexFxP4AoVsaDJAGvMcoJ4UAM=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
@@ -148,6 +182,18 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mr-karan/balance v0.0.0-20250317053523-d32c6ade6cf1 h1:YYLFUQMdeCyUVUAxE/IkPbkDfOVkUl9mbBdwkzU4UbE=
github.com/mr-karan/balance v0.0.0-20250317053523-d32c6ade6cf1/go.mod h1:YMjMm+2l1ye+v1MeuUJ1QPxXKzWp+x8iqg4vWuKB3Ao=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
@@ -158,6 +204,10 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -168,6 +218,8 @@ github.com/pkoukk/tiktoken-go-loader v0.0.2 h1:LUKws63GV3pVHwH1srkBplBv+7URgmOmh
github.com/pkoukk/tiktoken-go-loader v0.0.2/go.mod h1:4mIkYyZooFlnenDlormIo6cd5wrlUKNr97wp9nGgEKo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM=
github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rhnvrm/simples3 v0.10.1 h1:V/EbG6tC3F5MS7Vw5D02qIbNJom2jU+FVtGdrmsD67o=
@@ -175,6 +227,12 @@ github.com/rhnvrm/simples3 v0.10.1/go.mod h1:c2xW30bukipkBlWNnXG1wDjq3gykQ6ww2AB
github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
@@ -185,6 +243,12 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
@@ -203,6 +267,12 @@ github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0 h1:DtP3AflFjGTyHjXgd3UxuFfvGqk0rmfzm9PRbuiYwjU=
github.com/zerodha/fastcache/stores/goredis/v9 v9.1.0/go.mod h1:Pds6hwIUaduCzChTTS3JWKdXLhpdvjvF3r8rgB0huYk=
github.com/zerodha/fastcache/v4 v4.2.1 h1:MbkZv/lwSgAqoCnP+b023wxNlqHch0CU/B6RXaLgZKI=
github.com/zerodha/fastcache/v4 v4.2.1/go.mod h1:jfFLkiuyMIO8u7KlojdeLGaVyPuVXES0PRGxS7byr6s=
github.com/zerodha/fastglue v1.8.0 h1:yCfb8YwZLoFrzHiojRcie19olLDT48vjuinVn1Ge5Uc=
github.com/zerodha/fastglue v1.8.0/go.mod h1:+fB3j+iAz9Et56KapvdVoL79+m3h7NphR92TU4exWgk=
github.com/zerodha/logf v0.5.5 h1:AhxHlixHNYwhFjvlgTv6uO4VBKYKxx2I6SbHoHtWLBk=
@@ -211,6 +281,14 @@ github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0 h1:wkFp+/IM1cXjmidprZw8
github.com/zerodha/simplesessions/stores/redis/v3 v3.0.0/go.mod h1:HxUlesaeO/JuymHHoPQ+7GKVjrkCwEYiM/0+oyiaaDo=
github.com/zerodha/simplesessions/v3 v3.0.0 h1:seHwxVNnlCbp5nG8GFxSsRUdiHnfb39QdEW3J536O9Y=
github.com/zerodha/simplesessions/v3 v3.0.0/go.mod h1:lAK+CJmZRlbvfq+OnkB8Iyf6LWgjzvUuWYKX1XA51P0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -240,7 +318,6 @@ golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+8
View File
@@ -203,6 +203,7 @@ type queries struct {
GetPublishedArticlesByCollection *sqlx.Stmt `query:"get-published-articles-by-collection"`
SearchPublishedArticles *sqlx.Stmt `query:"search-published-articles"`
IncrementArticleViewCount *sqlx.Stmt `query:"increment-article-view-count"`
IncrementPublishedArticleView *sqlx.Stmt `query:"increment-published-article-view-count"`
InsertArticleFeedback *sqlx.Stmt `query:"insert-article-feedback"`
InsertSearchQuery *sqlx.Stmt `query:"insert-search-query"`
@@ -957,6 +958,13 @@ func (m *Manager) IncrementArticleViewCount(id int) {
}
}
// IncrementPublishedArticleView increments a published article's view count by its URL parts.
func (m *Manager) IncrementPublishedArticleView(helpCenterSlug, articleSlug, locale string) {
if _, err := m.q.IncrementPublishedArticleView.Exec(helpCenterSlug, articleSlug, locale); err != nil {
m.lo.Error("error incrementing article view count", "error", err, "slug", articleSlug)
}
}
// RecordArticleFeedback stores a reader's helpful/not-helpful vote for a published article.
func (m *Manager) RecordArticleFeedback(articleID int, isHelpful bool) error {
if _, err := m.q.InsertArticleFeedback.Exec(articleID, isHelpful); err != nil {
+7
View File
@@ -424,6 +424,13 @@ UPDATE help_articles
SET view_count = view_count + 1
WHERE id = $1;
-- name: increment-published-article-view-count
UPDATE help_articles a
SET view_count = view_count + 1
FROM article_collections c, help_centers h
WHERE a.collection_id = c.id AND c.help_center_id = h.id
AND h.slug = $1 AND a.slug = $2 AND a.locale = $3 AND a.status = 'published';
-- name: insert-article-feedback
INSERT INTO help_article_feedback (article_id, is_helpful)
SELECT $1, $2
+1 -5
View File
@@ -6,14 +6,10 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<base target="_blank">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet">
<style>
body {
background-color: #f4f4f5;
font-family: 'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
font-family: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
font-size: 15px;
line-height: 1.6;
margin: 0;
+17
View File
@@ -0,0 +1,17 @@
/* Geist (SIL OFL 1.1) - see fonts/LICENSE.txt. The italic face is only
downloaded when italic text is actually rendered on the page. */
@font-face {
font-family: 'Geist';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('/static/public/static/fonts/Geist-Variable.woff2') format('woff2');
}
@font-face {
font-family: 'Geist';
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('/static/public/static/fonts/Geist-Italic-Variable.woff2') format('woff2');
}
Binary file not shown.
+92
View File
@@ -0,0 +1,92 @@
Copyright (c) 2023 Vercel, in collaboration with basement.studio
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+1 -1
View File
@@ -26,7 +26,7 @@ body {
min-height: 100dvh;
display: flex;
flex-direction: column;
font-family: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-family: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 16px;
color: var(--hc-ink);
background: var(--hc-page);
+1 -1
View File
@@ -27,7 +27,7 @@ body.hcd {
min-height: 100dvh;
display: flex;
flex-direction: column;
font-family: "Instrument Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-family: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 16px;
color: var(--hc-ink);
background: var(--hc-bg);
+1 -3
View File
@@ -1,5 +1,3 @@
@import url("https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600&display=swap");
:root {
--primary-color: #1a1a1d;
--secondary-color: #f4f4f5;
@@ -22,7 +20,7 @@ body {
body {
background: #f9f9f9;
font-family: "Instrument Sans", "Inter", sans-serif;
font-family: "Geist", "Inter", sans-serif;
font-size: 16px;
line-height: 1.6;
color: var(--text-color);
+3 -2
View File
@@ -4,10 +4,11 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preload" href="/static/public/static/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin />
<link rel="stylesheet" href="/static/public/static/fonts.css?v={{ AssetVer }}" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@import url("https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600&display=swap");
body { font-family: "Instrument Sans", "Inter", sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: transparent; }
body { font-family: "Geist", "Inter", sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; background: transparent; }
.stars { display: flex; justify-content: center; gap: 8px; font-size: 2.2em; cursor: pointer; }
.star { color: #ddd; transition: color 0.15s ease, transform 0.15s ease; }
.star:hover, .star.sel { color: #f59e0b; transform: scale(1.15); }
@@ -25,9 +25,8 @@
{{ if .Data.OGImage }}<meta name="twitter:image" content="{{ .Data.OGImage }}" />{{ end }}
<meta name="theme-color" content="{{ .Data.HelpCenter.Color }}" />
{{ if .Data.HelpCenter.Theme.Favicon }}<link rel="icon" href="{{ .Data.HelpCenter.Theme.Favicon }}" />{{ else if ne FaviconURL "" }}<link rel="icon" href="{{ FaviconURL }}" />{{ end }}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400..700&display=swap" />
<link rel="preload" href="/static/public/static/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin />
<link rel="stylesheet" href="/static/public/static/fonts.css?v={{ AssetVer }}" />
<link rel="stylesheet" href="/static/public/static/help-center-cards.css?v={{ AssetVer }}" />
<link rel="stylesheet" href="/static/public/static/help-center-{{ .Data.HelpCenter.Template }}.css?v={{ AssetVer }}" />
<link rel="stylesheet" href="/static/public/static/article-content.css?v={{ AssetVer }}" />
+2
View File
@@ -6,6 +6,8 @@
<title>{{ .Data.Title }} - {{ SiteName }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<link rel="preload" href="/static/public/static/fonts/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin />
<link href="/static/public/static/fonts.css?v={{ AssetVer }}" rel="stylesheet" type="text/css" />
<link href="/static/public/static/style.css?v={{ AssetVer }}" rel="stylesheet" type="text/css" />
{{ if ne FaviconURL "" }}