mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 06:05:41 +00:00
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:
+20
-20
@@ -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
@@ -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())
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
@layer base {
|
||||
html,
|
||||
body {
|
||||
font-family: 'Instrument Sans', sans-serif;
|
||||
@apply font-sans;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
Binary file not shown.
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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 }}" />
|
||||
|
||||
@@ -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 "" }}
|
||||
|
||||
Reference in New Issue
Block a user