веб-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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user