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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

# Site
SITE_NAME=Example Blog
# Author in JSON-LD and RSS. Default: SITE_NAME.
AUTHOR_NAME=
SITE_URL=https://example.com
DEFAULT_LANG=en
GITHUB_URL=https://github.com/example
Expand Down Expand Up @@ -30,7 +32,9 @@ POPULAR_DAYS=30
POPULAR_LIMIT=5
PAGE_SIZE=10
RSS_LIMIT=20
# Link preview image for posts without a cover, a file inside assets/ (e.g. assets/og.png).
DEFAULT_OG_IMAGE=
# Optional, enables IndexNow pings: 8-128 characters a-z, A-Z, 0-9 or -.
INDEXNOW_KEY=

# Logs: debug | info | warn | error; json | text
Expand Down
11 changes: 8 additions & 3 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Proposed variables (final names set during implementation):
| Variable | Purpose |
|---|---|
| `SITE_NAME`, `SITE_URL` | Site name and public URL. |
| `AUTHOR_NAME` | Optional. Author in JSON-LD and RSS. Default: `SITE_NAME`. |
| `DEFAULT_LANG` | UI language and default post language: `en` or `ru`. Default `en`. |
| `GITHUB_URL` | Link in the top bar. |
| `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` | Bucket access. `S3_ENDPOINT` must be `https://`. `S3_REGION` default: `garage`. |
Expand All @@ -96,8 +97,8 @@ Proposed variables (final names set during implementation):
| `POPULAR_DAYS`, `POPULAR_LIMIT` | "Popular" window and size. Defaults: `30`, `5`. |
| `PAGE_SIZE` | Posts per page in "Newest" and tag pages. Default: `10`. |
| `RSS_LIMIT` | Posts in the RSS feed. Default: `20`. |
| `DEFAULT_OG_IMAGE` | Preview image for posts without `cover`. |
| `INDEXNOW_KEY` | Optional. Enables IndexNow pings. |
| `DEFAULT_OG_IMAGE` | Optional. Preview image for pages and posts without `cover`: a file inside `assets/`, same rules as `cover`. |
| `INDEXNOW_KEY` | Optional. Enables IndexNow pings. 8–128 characters `a-z A-Z 0-9 -`. Served at `/<INDEXNOW_KEY>.txt`. |
| `RATE_LIMIT_SEARCH` | Search limit per client. Default: `30/1m`. |
| `VISITOR_SECRET` | HMAC key for the daily visitor hash, ≥ 32 bytes. |
| `ALTCHA_EXPIRES`, `ALTCHA_MAX_NUMBER` | Challenge lifetime and difficulty. Defaults: `5m`, `100000`. |
Expand Down Expand Up @@ -189,6 +190,7 @@ Rules:
- No `title` or `date`, or invalid YAML → post skipped, WARN log.
- Invalid `date`, `updated`, `slug`, `lang` (not two lower-case letters), `cover` (not an allowed file in `assets/`) or `draft` → post skipped, WARN log.
- Slug from the file name: lower case, Russian letters transliterated (`ж` → `zh`, `х` → `kh`, `щ` → `shch`, `ъ` and `ь` dropped), any other character run → `-`. Heading anchors use the same rule.
- `tags`: lower-cased (`Go` → `go`), duplicates merged. Letters of any script, digits, `-`, `_`, `/`; 1–50 characters each; at most 20 per post. Anything else → post skipped, WARN log.
- Two posts with one `slug` → the first by object key wins, the second is skipped, ERROR log.
- Unknown fields are ignored. Obsidian can add its own.
- README describes every field in this table.
Expand Down Expand Up @@ -288,6 +290,7 @@ Goal: the best search possible without an extra service.
- Result snippets with highlighted matches.
- Works without JS: `GET /search?q=…` renders results. With JS: results update while typing.
- Filter by tag.
- The results page has `noindex`.

Rejected: Meilisearch and similar. Better typo handling, but a separate service.

Expand Down Expand Up @@ -417,7 +420,8 @@ 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 |
| `/static/*` | GET | public | embedded CSS and fonts only, no directory listing |
| `/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 |

Other methods → `405`. Unknown paths → `404`.

Expand Down Expand Up @@ -610,3 +614,4 @@ Other methods → `405`. Unknown paths → `404`.
- 2026-11-03 — Config defaults: `POLL_INTERVAL` 60s, `READING_WPM` 200, `LOG_LEVEL` info, `LOG_FORMAT` json, `DATA_DIR` /data, `VAULT_DIR` /vault, `ALTCHA_MAX_NUMBER` 100000, `S3_REGION` garage. Log component `main`. The service listens on fixed port 8080.
- 2026-11-03 — Sync: S3 client `minio-go/v7`, frontmatter via `go.yaml.in/yaml/v3`, GFM. Mirror state in `VAULT_DIR/.hyperhack.json`. S3 down at startup: serve from the mirror; exit 1 only with no mirror. No Remotely Save prefix. Keys with a `.` or `_` segment are ignored (was proposed). `MAX_OBJECTS` counts all keys. An invalid frontmatter field skips the post. 3 S3 attempts per poll. `/reindex` waits for the sync and returns `204`.
- 2026-11-04 — Pages: theme and language switch via a no-JS form `POST /prefs`; cookies `theme` / `lang` (`HttpOnly; SameSite=Lax; Max-Age=1 year`, `Secure` on https). Logo from `assets/logo.svg`, built-in mark otherwise. Fonts Inter + JetBrains Mono, self-hosted. Pagination `/?page=N`: page 1 is `/`, other out-of-range values → `404`. `<html lang>` from the post; the top bar keeps the UI language. CSS and fonts at `/static/*`.
- 2026-11-04 — Search, tags, RSS, SEO: Bleve `v2.6.1`, in-memory index, one document mapping per language (`en`, `ru`). Fuzziness by word length: ≤ 3 letters 0, ≤ 6 letters 1, longer 2; prefix on the last word. Snippets keep only `<mark>`. Search works without JS; `search.js` swaps the results while typing (debounced). Results page `noindex`. Tags lower case, Obsidian characters, `/tags/<tag>` paginated like the main page. 24 tag hue buckets (FNV-1a hash), contrast checked in tests. RSS: `dc:creator` from `AUTHOR_NAME`, `atom:updated` from `updated`. Image `width`/`height` read from the mirror (`golang.org/x/image` for WebP); an asset change re-renders all posts. IndexNow: one ping per sync with new, changed and removed post URLs, not on the first index load. New `AUTHOR_NAME`; `DEFAULT_OG_IMAGE` is a path inside `assets/`.
11 changes: 8 additions & 3 deletions SPEC.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
| Переменная | Назначение |
|---|---|
| `SITE_NAME`, `SITE_URL` | Название и публичный URL сайта. |
| `AUTHOR_NAME` | Необязательно. Автор в JSON-LD и RSS. По умолчанию: `SITE_NAME`. |
| `DEFAULT_LANG` | Язык интерфейса и язык поста по умолчанию: `en` или `ru`. По умолчанию `en`. |
| `GITHUB_URL` | Ссылка в верхнем баре. |
| `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` | Доступ к бакету. `S3_ENDPOINT` только `https://`. `S3_REGION` по умолчанию: `garage`. |
Expand All @@ -96,8 +97,8 @@
| `POPULAR_DAYS`, `POPULAR_LIMIT` | Окно и размер «Популярных». По умолчанию: `30`, `5`. |
| `PAGE_SIZE` | Постов на страницу в «Новых» и на страницах тегов. По умолчанию: `10`. |
| `RSS_LIMIT` | Постов в RSS. По умолчанию: `20`. |
| `DEFAULT_OG_IMAGE` | Картинка превью для постов без `cover`. |
| `INDEXNOW_KEY` | Необязательно. Включает пинги IndexNow. |
| `DEFAULT_OG_IMAGE` | Необязательно. Картинка превью для страниц и постов без `cover`: файл внутри `assets/`, правила как у `cover`. |
| `INDEXNOW_KEY` | Необязательно. Включает пинги IndexNow. 8–128 символов `a-z A-Z 0-9 -`. Отдаётся на `/<INDEXNOW_KEY>.txt`. |
| `RATE_LIMIT_SEARCH` | Лимит поиска на клиента. По умолчанию: `30/1m`. |
| `VISITOR_SECRET` | HMAC-ключ для суточного хеша посетителя, ≥ 32 байт. |
| `ALTCHA_EXPIRES`, `ALTCHA_MAX_NUMBER` | Срок жизни и сложность задачи. По умолчанию: `5m`, `100000`. |
Expand Down Expand Up @@ -189,6 +190,7 @@ draft: false
- Нет `title` или `date`, или YAML не парсится → пост пропускается, лог WARN.
- Неверные `date`, `updated`, `slug`, `lang` (не две строчные буквы), `cover` (не разрешённый файл в `assets/`) или `draft` → пост пропускается, лог WARN.
- Slug из имени файла: строчные буквы, русские буквы транслитерируются (`ж` → `zh`, `х` → `kh`, `щ` → `shch`, `ъ` и `ь` убираются), любая другая последовательность символов → `-`. Якоря заголовков строятся так же.
- `tags`: приводятся к нижнему регистру (`Go` → `go`), дубли сливаются. Буквы любого алфавита, цифры, `-`, `_`, `/`; 1–50 символов каждый; не больше 20 на пост. Иное → пост пропускается, лог WARN.
- Два поста с одним `slug` → публикуется первый по ключу объекта, второй пропускается, лог ERROR.
- Неизвестные поля игнорируются. Obsidian может добавлять свои.
- README описывает все поля этой таблицы.
Expand Down Expand Up @@ -288,6 +290,7 @@ Garage не шлёт уведомления об изменениях в бак
- Сниппеты с подсветкой совпадений.
- Работает без JS: `GET /search?q=…` рендерит результаты. С JS: результаты обновляются при наборе.
- Фильтр по тегу.
- У страницы результатов `noindex`.

Отклонено: Meilisearch и подобные. Опечатки ловит лучше, но это отдельный сервис.

Expand Down Expand Up @@ -417,7 +420,8 @@ Garage не шлёт уведомления об изменениях в бак
| `/metrics` | GET | приватная сеть | блок в Caddy, `METRICS_ALLOW` |
| `/healthz` | GET | публичный | постоянный ответ, без данных |
| `/prefs` | POST | публичный | тело формы ≤ 4 КБ, проверка перечислений `theme` / `lang`, редирект только на локальный путь |
| `/static/*` | GET | публичный | только встроенные CSS и шрифты, без листинга папок |
| `/static/*` | GET | публичный | только встроенные CSS, JS и шрифты, без листинга папок |
| `/<INDEXNOW_KEY>.txt` | GET | публичный | только если задан `INDEXNOW_KEY`; в теле — ключ |

Другие методы → `405`. Неизвестные пути → `404`.

Expand Down Expand Up @@ -610,3 +614,4 @@ Garage не шлёт уведомления об изменениях в бак
- 2026-11-03 — Значения по умолчанию: `POLL_INTERVAL` 60s, `READING_WPM` 200, `LOG_LEVEL` info, `LOG_FORMAT` json, `DATA_DIR` /data, `VAULT_DIR` /vault, `ALTCHA_MAX_NUMBER` 100000, `S3_REGION` garage. Компонент логов `main`. Сервис слушает фиксированный порт 8080.
- 2026-11-03 — Синхронизация: клиент S3 `minio-go/v7`, frontmatter через `go.yaml.in/yaml/v3`, GFM. Состояние зеркала в `VAULT_DIR/.hyperhack.json`. S3 недоступен при старте: работаем из зеркала; выход с кодом 1 только без зеркала. Без префикса Remotely Save. Ключи с сегментом на `.` или `_` игнорируются (было предложено). `MAX_OBJECTS` считает все ключи. Неверное поле frontmatter — пост пропускается. 3 попытки S3 за опрос. `/reindex` ждёт синхронизацию и возвращает `204`.
- 2026-11-04 — Страницы: тема и язык переключаются формой без JS `POST /prefs`; cookie `theme` / `lang` (`HttpOnly; SameSite=Lax; Max-Age=1 год`, `Secure` на https). Логотип из `assets/logo.svg`, иначе встроенный знак. Шрифты Inter + JetBrains Mono, свои. Пагинация `/?page=N`: страница 1 — это `/`, другие значения вне диапазона → `404`. `<html lang>` из поста; верхний бар остаётся на языке интерфейса. CSS и шрифты на `/static/*`.
- 2026-11-04 — Поиск, теги, RSS, SEO: Bleve `v2.6.1`, индекс в памяти, своя схема документа на каждый язык (`en`, `ru`). Опечатки по длине слова: ≤ 3 букв 0, ≤ 6 букв 1, длиннее 2; префикс по последнему слову. В сниппетах остаётся только `<mark>`. Поиск работает без JS; `search.js` подменяет результаты при наборе (с задержкой). У страницы результатов `noindex`. Теги в нижнем регистре, символы как в Obsidian, `/tags/<tag>` с пагинацией как на главной. 24 оттенка для тегов (хеш FNV-1a), контраст проверяется в тестах. RSS: `dc:creator` из `AUTHOR_NAME`, `atom:updated` из `updated`. `width`/`height` картинок читаются из зеркала (`golang.org/x/image` для WebP); изменение ассета перерисовывает все посты. IndexNow: один пинг за синхронизацию с URL новых, изменённых и удалённых постов, не при первой загрузке индекса. Новая `AUTHOR_NAME`; `DEFAULT_OG_IMAGE` — путь внутри `assets/`.
27 changes: 27 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,59 @@ toolchain go1.27.3

require (
github.com/alecthomas/chroma/v2 v2.27.0
github.com/blevesearch/bleve/v2 v2.6.1
github.com/microcosm-cc/bluemonday v1.0.27
github.com/minio/minio-go/v7 v7.4.1
github.com/yuin/goldmark v1.8.7
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.yaml.in/yaml/v3 v3.0.6
golang.org/x/image v0.47.0
golang.org/x/net v0.62.0
)

require (
github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bits-and-blooms/bitset v1.24.2 // indirect
github.com/blevesearch/bleve_index_api v1.4.1 // indirect
github.com/blevesearch/geo v0.2.6 // indirect
github.com/blevesearch/go-faiss v1.1.5 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
github.com/blevesearch/mmap-go v1.2.0 // indirect
github.com/blevesearch/scorch_segment_api/v2 v2.4.10 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
github.com/blevesearch/vellum v1.2.0 // indirect
github.com/blevesearch/zapx/v11 v11.4.3 // indirect
github.com/blevesearch/zapx/v12 v12.4.3 // indirect
github.com/blevesearch/zapx/v13 v13.4.3 // indirect
github.com/blevesearch/zapx/v14 v14.4.3 // indirect
github.com/blevesearch/zapx/v15 v15.4.3 // indirect
github.com/blevesearch/zapx/v16 v16.3.4 // indirect
github.com/blevesearch/zapx/v17 v17.2.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect
github.com/klauspost/compress v1.19.2 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
golang.org/x/crypto v0.58.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/ini.v1 v1.67.3 // indirect
)
Loading
Loading