веб-ui: знак «jb» стал единым для шапки, иконки вкладки и превью ссылки

- логотип сведён в web/static/logo.svg (буквы геометрией, оптическая
  центровка); прежний favicon с «play» убран
- <head> шести страниц свёрнут в партиал head с og-мета; адреса абсолютные,
  база берётся из запроса, хост из недоверенного заголовка проверяется
- растровые ассеты og-cover.jpg и apple-touch-icon.png собирает `task brand`
  из исходников web/*.html
This commit is contained in:
av
2026-09-02 10:55:45 +03:00
parent 3bce73fc34
commit 2971a520c7
24 changed files with 339 additions and 51 deletions
+77
View File
@@ -0,0 +1,77 @@
package httpapi
import (
"net/http"
"strings"
)
// pageMeta — мета-шапка страницы: заголовок вкладки и адреса для превью
// ссылки (og:*). Заполняется только на страничных путях; htmx-фрагменты
// рендерят те же view с пустым pageMeta и <head> не отдают.
type pageMeta struct {
Title string // <title> и og:title
URL string // адрес страницы (og:url)
Image string // адрес карточки превью (og:image)
}
// pageMetaFor собирает мету страницы. Адреса делаются абсолютными: краулер
// мессенджера не обязан разрешать относительный og:image, а без картинки
// превью ссылки вырождается в голый заголовок.
func (s *server) pageMetaFor(r *http.Request, title string) pageMeta {
base := absBase(r)
return pageMeta{
Title: title,
URL: base + r.URL.EscapedPath(),
Image: base + "/static/og-cover.jpg?v=" + s.assetVer,
}
}
// absBase — база адреса («https://host»), какой её видит клиент: за обратным
// прокси схему и хост несут X-Forwarded-Proto/X-Forwarded-Host, иначе берём
// соединение и Host запроса. Значение уходит только в мета-теги превью,
// доменных решений на нём не строится. Хост непохожей формы отбрасываем
// целиком: пустая база оставляет в мете корректный относительный адрес.
func absBase(r *http.Request) string {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
switch v := firstForwarded(r.Header.Get("X-Forwarded-Proto")); v {
case "http", "https":
scheme = v
}
host := r.Host
if v := firstForwarded(r.Header.Get("X-Forwarded-Host")); v != "" {
host = v
}
if !validHost(host) {
return ""
}
return scheme + "://" + host
}
// firstForwarded берёт первое значение списка «a, b, c»: цепочка прокси
// дописывает свои значения справа, клиенту ближе левое.
func firstForwarded(v string) string {
if i := strings.IndexByte(v, ','); i >= 0 {
v = v[:i]
}
return strings.TrimSpace(v)
}
// validHost — проверка хоста из недоверенного заголовка: допустимы только
// знаки имени, порта и скобок IPv6.
func validHost(host string) bool {
if host == "" || len(host) > 255 {
return false
}
for _, c := range host {
switch {
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
case c == '.', c == '-', c == '_', c == ':', c == '[', c == ']':
default:
return false
}
}
return true
}