package httpapi import ( "net/http" "strings" ) // pageMeta — мета-шапка страницы: заголовок вкладки и адреса для превью // ссылки (og:*). Заполняется только на страничных путях; htmx-фрагменты // рендерят те же view с пустым pageMeta и не отдают. type pageMeta struct { Title string // и 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 }