Files
libredesk/internal/stringutil/htmlimages.go
T
Abhinav Raut 68bb85381c fix help center code review findings across public pages and admin
A batch of correctness fixes from a code review of the help center branch.

Public pages:
- hide the article author in JSON-LD when the theme hides the byline, so
  the author name no longer leaks in structured data.
- make agent and AI assistant avatars public on upload (and backfill AI
  assistants in the v2.7.0 migration) so author avatars don't 401 for
  anonymous visitors.
- return the same 401 for missing and existing private uploads to an
  anonymous caller, so /uploads can't be used to probe which private
  media exists.
- switch the bare /hc/{slug} home redirect from 301 to 302 since the
  default locale is mutable and a permanent redirect gets cached.
- match the request host against the public URL by hostname, ignoring
  any port, so robots.txt and sitemaps resolve on custom domains.
- fix the lazy-load regex so an img with data-loading or alt text like
  "loading=x" still gets loading="lazy" and decoding="async".
- stop logging searches shorter than the minimum length, which never
  actually run, so they don't pollute the no-result insights.

Admin and data integrity:
- take the help center row lock in UpdateArticle, MoveArticle,
  CreateCollection and UpdateCollection, and run the collection depth
  check inside that transaction, so concurrent edits can't create
  duplicate slugs or overshoot the max depth.
- default the collection locale before validating the parent, so
  creating a child collection without a locale no longer 400s.
- keep a hand-edited help center slug when the name is edited again.
- render the Snippets page title in plural.
- release the feedback dedup key when the insert fails, so a retry
  isn't swallowed as a duplicate.
2026-08-09 12:03:54 +05:30

48 lines
1.3 KiB
Go

package stringutil
import (
"regexp"
"strings"
)
// imgAttrPrefix skips over complete attributes so the name can't match inside another
// attribute's name (data-loading) or a quoted value (alt="loading=x").
const imgAttrPrefix = `(?is)^<img(?:\s+[^\s=>]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'>]*))?)*\s+`
var (
imgTagRe = regexp.MustCompile(`(?is)<img\b(?:"[^"]*"|'[^']*'|[^>"'])*>`)
imgLoadingAttrRe = regexp.MustCompile(imgAttrPrefix + `loading\s*=`)
imgDecodingAttrRe = regexp.MustCompile(imgAttrPrefix + `decoding\s*=`)
)
// DeferOffscreenImages adds loading="lazy" and decoding="async" to every <img> tag
// except the first, which is left eager since it is the likely LCP element.
func DeferOffscreenImages(html string) string {
n := 0
return imgTagRe.ReplaceAllStringFunc(html, func(tag string) string {
n++
if n == 1 {
return tag
}
var attrs strings.Builder
if !imgLoadingAttrRe.MatchString(tag) {
attrs.WriteString(` loading="lazy"`)
}
if !imgDecodingAttrRe.MatchString(tag) {
attrs.WriteString(` decoding="async"`)
}
if attrs.Len() == 0 {
return tag
}
closing := ">"
body := strings.TrimSuffix(tag, ">")
if trimmed, ok := strings.CutSuffix(body, "/"); ok {
body = trimmed
closing = " />"
}
return strings.TrimRight(body, " \t\r\n") + attrs.String() + closing
})
}