diff --git a/README.md b/README.md index ff9ffd4..6641051 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Rules: - Unknown fields are ignored, so Obsidian can add its own. - Images: standard Markdown, `![Alt text](assets/photo.webp)`. A path `assets/…` becomes `/assets/…` on the site. External images are blocked by the CSP. - Allowed asset types: `png jpg jpeg webp avif gif svg mp4 webm`. -- `assets/logo.svg`, if present, replaces the built-in logo in the top bar. +- `assets/logo.svg`, if present, replaces the built-in logo in the top bar and the favicon. ## Publishing from Obsidian diff --git a/README.ru.md b/README.ru.md index 50caea6..8e81484 100644 --- a/README.ru.md +++ b/README.ru.md @@ -181,7 +181,7 @@ draft: false - Неизвестные поля игнорируются, так что Obsidian может добавлять свои. - Картинки — обычный Markdown, `![Подпись](assets/photo.webp)`. Путь `assets/…` на сайте становится `/assets/…`. Внешние картинки блокирует CSP. - Разрешённые типы ассетов: `png jpg jpeg webp avif gif svg mp4 webm`. -- `assets/logo.svg`, если есть, заменяет встроенный логотип в верхней панели. +- `assets/logo.svg`, если есть, заменяет встроенный логотип в верхней панели и фавиконку. ## Публикация из Obsidian diff --git a/SPEC.md b/SPEC.md index 49d0c75..26ceed0 100644 --- a/SPEC.md +++ b/SPEC.md @@ -120,7 +120,7 @@ Proposed variables (final names set during implementation): ## Bucket layout - `assets/` — all assets: covers, images in posts. Decided. -- `assets/logo.svg` — optional site logo in the top bar. Without it the service shows a built-in mark. +- `assets/logo.svg` — optional site logo in the top bar and favicon. Without it the service shows a built-in mark. - The service ignores keys where any path segment starts with `.` or `_` (same rule as Remotely Save). ### Posts @@ -421,6 +421,7 @@ Trust boundaries: internet → Caddy → service; bucket → service (semi-trust | `/metrics` | GET | private network | Caddy block, `METRICS_ALLOW` | | `/healthz` | GET | public | constant body, no data | | `/prefs` | POST | public | form body ≤ 4 KB, enum check for `theme` / `lang`, redirect only to a local path | +| `/favicon.ico` | GET | public | built-in SVG mark, or `302` to `/assets/logo.svg` when the bucket has a logo; never `404` | | `/static/*` | GET | public | embedded CSS, JS and fonts only, no directory listing | | `/.txt` | GET | public | only if `INDEXNOW_KEY` is set; body is the key | @@ -618,3 +619,4 @@ Other methods → `405`. Unknown paths → `404`. - 2026-11-04 — Grafana, Caddy, README: dashboard `grafana/hyperhack.json` for a Prometheus-type data source on VictoriaMetrics (uses the VictoriaMetrics function `sort_by_label_numeric`); daily bars cover the day that starts at the bar, so today is shown. `Caddyfile.example` for Caddy `v2.11.7` with `caddy-ratelimit` `v0.1.0` (1200 requests per minute per client IP; `/static/*` and `/assets/*` are not counted) and `caddy-crowdsec-bouncer` `v0.14.1`; `/metrics` and `/reindex` (and their subpaths) answer `404` on the public domain; compression `zstd`/`gzip`. README: behind Caddy on the host, `TRUSTED_PROXIES` is the gateway of the Compose network. Full README in English and Russian. - 2026-11-04 — Production Compose: image registry GHCR (confirmed). `docker-compose.yml` only pulls the released image, no `build:`, pinned to the latest release (`0.1.0`; image tags are the git tag without `v`). Local builds in `compose.dev.yml`, used only with `-f`; no `docker-compose.override.yml`. README Quick start uses the downloaded `docker-compose.yml` as is, and README documents upgrades and rollback. - 2026-11-04 — Post cover as a full-width banner between the top bar and the text column: height `min(30vh, 320px)` (`min(25vh, 200px)` on screens up to 30rem), `object-fit: cover` centered, no rounded corners, `width`/`height` kept. Rendered from a `banner` block in the layout, outside `
`. Posts without cover unchanged. +- 2026-11-04 — Favicon: every page has `` pointing to `/assets/logo.svg` when the bucket has it, else to `/favicon.ico`. `GET /favicon.ico` serves the built-in mark as SVG (accent background, white mark, fixed colors), or answers `302` to `/assets/logo.svg` when the logo exists; `Cache-Control: max-age=3600`, since the logo can change with any sync. Never `404`. diff --git a/SPEC.ru.md b/SPEC.ru.md index bc601d4..5c22ebf 100644 --- a/SPEC.ru.md +++ b/SPEC.ru.md @@ -120,7 +120,7 @@ ## Структура бакета - `assets/` — все ассеты: обложки, картинки в постах. Решено. -- `assets/logo.svg` — необязательный логотип сайта в верхнем баре. Без него сервис показывает встроенный знак. +- `assets/logo.svg` — необязательный логотип сайта в верхнем баре и фавиконка. Без него сервис показывает встроенный знак. - Сервис игнорирует ключи, где любой сегмент пути начинается с `.` или `_` (как Remotely Save). ### Посты @@ -421,6 +421,7 @@ Garage не шлёт уведомления об изменениях в бак | `/metrics` | GET | приватная сеть | блок в Caddy, `METRICS_ALLOW` | | `/healthz` | GET | публичный | постоянный ответ, без данных | | `/prefs` | POST | публичный | тело формы ≤ 4 КБ, проверка перечислений `theme` / `lang`, редирект только на локальный путь | +| `/favicon.ico` | GET | публичный | встроенный знак в SVG или `302` на `/assets/logo.svg`, если в бакете есть логотип; никогда не `404` | | `/static/*` | GET | публичный | только встроенные CSS, JS и шрифты, без листинга папок | | `/.txt` | GET | публичный | только если задан `INDEXNOW_KEY`; в теле — ключ | @@ -618,3 +619,4 @@ Garage не шлёт уведомления об изменениях в бак - 2026-11-04 — Grafana, Caddy, README: дашборд `grafana/hyperhack.json` для источника данных типа Prometheus на VictoriaMetrics (использует функцию VictoriaMetrics `sort_by_label_numeric`); дневной столбец покрывает день, который начинается в его точке, поэтому сегодняшний день виден. `Caddyfile.example` для Caddy `v2.11.7` с `caddy-ratelimit` `v0.1.0` (1200 запросов в минуту на IP клиента; `/static/*` и `/assets/*` не учитываются) и `caddy-crowdsec-bouncer` `v0.14.1`; `/metrics` и `/reindex` (и их подпути) на публичном домене отвечают `404`; сжатие `zstd`/`gzip`. README: если Caddy на хосте, `TRUSTED_PROXIES` — шлюз сети Compose. Полный README на английском и русском. - 2026-11-04 — Продакшен Compose: реестр образов GHCR (подтверждено). `docker-compose.yml` только скачивает выпущенный образ, без `build:`, закреплён последний релиз (`0.1.0`; тег образа — git-тег без `v`). Локальная сборка в `compose.dev.yml`, только через `-f`; без `docker-compose.override.yml`. Быстрый старт в README использует скачанный `docker-compose.yml` без правок, README описывает обновление и откат. - 2026-11-04 — Обложка поста — баннер на всю ширину между верхней панелью и колонкой текста: высота `min(30vh, 320px)` (`min(25vh, 200px)` на экранах до 30rem), `object-fit: cover` по центру, без скруглений, `width`/`height` сохраняются. Выводится из блока `banner` в шаблоне страницы, вне `
`. Посты без обложки не меняются. +- 2026-11-04 — Фавиконка: на каждой странице `` ведёт на `/assets/logo.svg`, если он есть в бакете, иначе на `/favicon.ico`. `GET /favicon.ico` отдаёт встроенный знак в SVG (фон акцентного цвета, белый знак, цвета фиксированные) или отвечает `302` на `/assets/logo.svg`, если логотип есть; `Cache-Control: max-age=3600`, потому что логотип может измениться с любой синхронизацией. Никогда не `404`. diff --git a/internal/server/pages.go b/internal/server/pages.go index aa63a2a..f2c9d3a 100644 --- a/internal/server/pages.go +++ b/internal/server/pages.go @@ -92,6 +92,7 @@ type pages struct { tmpl map[string]map[string]*template.Template // lang → page → template css []byte cssV string + mark []byte // built-in mark, for /favicon.ico scripts map[string]script // file name → script fonts fs.FS @@ -162,6 +163,9 @@ func newPages(log *slog.Logger, cfg *config.Config, d Deps) *pages { } p.scripts[name] = script{body: b, url: versioned("/static/"+name, b)} } + if p.mark, err = webFS.ReadFile("web/static/favicon.svg"); err != nil { + panic(err) + } p.fonts, err = fs.Sub(webFS, "web/static/fonts") if err != nil { panic(err) @@ -587,6 +591,17 @@ func (p *pages) style(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write(p.css) } +// favicon never answers 404: it redirects to the bucket logo when there is one, else serves the built-in mark. +func (p *pages) favicon(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "public, max-age=3600") // the logo can appear or go with any sync + if ix := p.index(); ix != nil && ix.Logo { + http.Redirect(w, r, "/assets/logo.svg", http.StatusFound) + return + } + w.Header().Set("Content-Type", "image/svg+xml") + _, _ = w.Write(p.mark) +} + func (p *pages) font(w http.ResponseWriter, r *http.Request) { name := r.PathValue("file") if ext := path.Ext(name); ext != ".woff2" && ext != ".txt" { diff --git a/internal/server/pages_test.go b/internal/server/pages_test.go index c91658d..93cdf21 100644 --- a/internal/server/pages_test.go +++ b/internal/server/pages_test.go @@ -274,3 +274,23 @@ func TestStatic(t *testing.T) { } } } + +func TestFavicon(t *testing.T) { + ix := testIndex(1) + s := newSite(t, testConfig(), ix) + mustContain(t, s.get("/").Body.String(), ``) + for _, idx := range []*content.Index{ix, nil} { // nil: still starting, no index yet + s.idx.Store(idx) + rec := s.get("/favicon.ico") + if rec.Code != 200 || rec.Header().Get("Content-Type") != "image/svg+xml" || !strings.HasPrefix(rec.Body.String(), "