Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
| `/<INDEXNOW_KEY>.txt` | GET | public | only if `INDEXNOW_KEY` is set; body is the key |

Expand Down Expand Up @@ -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 `<main>`. Posts without cover unchanged.
- 2026-11-04 — Favicon: every page has `<link rel="icon" type="image/svg+xml">` 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`.
4 changes: 3 additions & 1 deletion SPEC.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
## Структура бакета

- `assets/` — все ассеты: обложки, картинки в постах. Решено.
- `assets/logo.svg` — необязательный логотип сайта в верхнем баре. Без него сервис показывает встроенный знак.
- `assets/logo.svg` — необязательный логотип сайта в верхнем баре и фавиконка. Без него сервис показывает встроенный знак.
- Сервис игнорирует ключи, где любой сегмент пути начинается с `.` или `_` (как Remotely Save).

### Посты
Expand Down Expand Up @@ -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 и шрифты, без листинга папок |
| `/<INDEXNOW_KEY>.txt` | GET | публичный | только если задан `INDEXNOW_KEY`; в теле — ключ |

Expand Down Expand Up @@ -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` в шаблоне страницы, вне `<main>`. Посты без обложки не меняются.
- 2026-11-04 — Фавиконка: на каждой странице `<link rel="icon" type="image/svg+xml">` ведёт на `/assets/logo.svg`, если он есть в бакете, иначе на `/favicon.ico`. `GET /favicon.ico` отдаёт встроенный знак в SVG (фон акцентного цвета, белый знак, цвета фиксированные) или отвечает `302` на `/assets/logo.svg`, если логотип есть; `Cache-Control: max-age=3600`, потому что логотип может измениться с любой синхронизацией. Никогда не `404`.
15 changes: 15 additions & 0 deletions internal/server/pages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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" {
Expand Down
20 changes: 20 additions & 0 deletions internal/server/pages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(), `<link rel="icon" type="image/svg+xml" href="/favicon.ico">`)
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(), "<svg") {
t.Errorf("built-in favicon = %d %q", rec.Code, rec.Header().Get("Content-Type"))
}
}

ix.Logo = true
s.idx.Store(ix)
mustContain(t, s.get("/").Body.String(), `<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">`)
if rec := s.get("/favicon.ico"); rec.Code != http.StatusFound || rec.Header().Get("Location") != "/assets/logo.svg" {
t.Errorf("favicon with logo = %d %q", rec.Code, rec.Header().Get("Location"))
}
}
1 change: 1 addition & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func New(log *slog.Logger, cfg *config.Config, d Deps) http.Handler {
mux.HandleFunc("GET /sitemap.xml", p.sitemap)
mux.HandleFunc("GET /robots.txt", p.robots)
mux.HandleFunc("GET /llms.txt", p.llms)
mux.HandleFunc("GET /favicon.ico", p.favicon)
if key := cfg.IndexNowKey; key != "" { // the config allows only [a-zA-Z0-9-]
mux.HandleFunc("GET /"+key+".txt", func(w http.ResponseWriter, _ *http.Request) {
writeText(w, "text/plain; charset=utf-8", key)
Expand Down
1 change: 1 addition & 0 deletions internal/server/web/static/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions internal/server/web/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
{{- with .JSONLD}}
<script type="application/ld+json">{{.}}</script>
{{- end}}
<link rel="icon" type="image/svg+xml" href="{{if .Site.Logo}}/assets/logo.svg{{else}}/favicon.ico{{end}}">
<link rel="stylesheet" href="{{.CSS}}">
{{- range .JS}}
<script src="{{.}}" defer></script>
Expand Down
Loading