routes
Маршруты (route/routes)
Концепция
Поля route/routes во frontmatter позволяют заметке быть доступной по произвольным URL — на главном домене или на кастомных доменах — не меняя её permalink в nv.Map.
Ключевое разграничение:
| Поле | Меняет permalink в nv.Map |
Попадает в RouteMap |
Назначение |
|---|---|---|---|
slug |
Да | Нет | Обратная совместимость, кастомный URL заметки |
route/routes |
Нет | Да | Алиасы на главном домене, кастомные домены |
slug и route полностью независимы: можно использовать оба на одной заметке.
Frontmatter
# Алиас на главном домене (без хоста)
route: /about
# Корень кастомного домена
route: mysite.com/
# Кастомный домен, конкретный путь
route: mysite.com/hello
# Несколько маршрутов (mix главный + кастомный)
routes:
- /my-alias
- mysite.com/
- other.com/landing
Правила парсинга
| Значение | host | path | Поведение |
|---|---|---|---|
/about |
"" |
/about |
Алиас на главном домене |
/ |
"" |
/ |
Алиас на корне главного домена |
foo.com |
foo.com |
"" |
Кастомный домен, path = permalink заметки |
foo.com/ |
foo.com |
/ |
Кастомный домен, явный корень |
foo.com/hello |
foo.com |
/hello |
Кастомный домен, конкретный путь |
localhost:8081/p |
localhost:8081 |
/p |
Dev-режим с портом |
Нормализация домена: lowercase + удаление www.. Реализация: model.NormalizeDomain().
Пример: www.FOO.COM/about → host=foo.com, path=/about.
Разрешение запроса
resolveNote() в internal/case/rendernotepage/resolve.go:
запрос пришёл на host H, path P
│
├── H == главный домен (или пустой)?
│ ├── RouteMap[""][P] → нашли → отдаём
│ └── nv.Map[P] → нашли → отдаём
│
└── H == кастомный домен
├── RouteMap[H][P] → нашли → отдаём
└── nv.Map[P] → fallthrough по permalink
Важно: Алиасы главного домена (route: /x) не доступны на кастомном домене по тому же пути. RouteMap[""]["/x"] не используется при запросе с H != главный_домен.
Fallthrough на кастомном домене: если маршрут не найден в RouteMap[H], сервер всё равно ищет заметку в nv.Map по permalink. Это означает, что посетители кастомного домена могут получить доступ к любой публичной заметке по её permalink — не только к явно прописанным в route.
Приоритет при коллизиях
route: /на главном домене перекрывает_index.mdвRouteMap, ноnv.Map["/"]по-прежнему указывает на_index.md(роут не трогаетnv.Map)- Два маршрута с одинаковым host+path → побеждает последний зарегистрированный (порядок загрузки заметок)
slug+routeна одной заметке — независимы, не конфликтуют
OG теги
На кастомном домене og:url формируется из маршрута, а не из permalink.
Логика (buildOGTags → ogURLForNote в endpoint.go):
- Если запрос пришёл с кастомного домена — ищем в
note.Routesмаршрут сHost == requestHost - Предпочтение: точное совпадение host+path; иначе первый маршрут с нужным host
- Схема берётся из
env.PublicURL()(http/https)
Sitemap для кастомных доменов
При перезагрузке заметок (noteloader/loader.go) генерируются:
nvs.Sitemap— основной sitemap для главного домена (всеfree: trueзаметки по permalink)nvs.DomainSitemaps[domain]— отдельный sitemap для каждого кастомного домена
Sitemap кастомного домена содержит только заметки с явными маршрутами на этот домен. URL в sitemap строятся как <domainURL><route.Path>.
Запрос /sitemap.xml с заголовком Host: foo.com отдаёт DomainSitemaps["foo.com"], если он существует.
Frontmatter patches
route/routes извлекаются после применения всех патчей (в ExtractMetaData() → ExtractRoutes()). Это значит, что патч может добавить маршрут к заметке:
# Патч для multidomain/landing.md:
{ "route": "mydomain.com/" }
Подробнее: docs/frontmatter_patches.md → раздел «Маршруты и патчи».
Ограничения
TLS. Кастомные домены требуют внешней TLS-терминации (reverse proxy, Cloudflare и т.п.). Список ACME-доменов в конфиге статичен и не синхронизируется с RouteMap автоматически.
Cookies / авторизация. Браузерная политика CORS/cookies: пользователь, авторизованный на главном домене, будет гостем на кастомном. Для контента под paywall используйте free: true.
TrustedDomains. Кастомные домены из RouteMap не добавляются автоматически в TrustedDomains() (используется при валидации redirect URL после Telegram-авторизации).
Структуры данных
// internal/model/note_routes.go
type ParsedRoute struct {
Host string // "" = главный домен; "foo.com" = кастомный
Path string // "" = использовать Permalink заметки; "/x" = явный путь
}
func ParseRoute(value string) ParsedRoute
func NormalizeDomain(d string) string
func ExtractHost(rawURL string) string
// internal/model/note.go
type NoteView struct {
// ...
Routes []ParsedRoute // из frontmatter route/routes; не влияет на Permalink
}
type NoteViews struct {
Map map[string]*NoteView // keyed by Permalink
PathMap map[string]*NoteView // keyed by file path
RouteMap map[string]map[string]*NoteView // host → path → note
Sitemap []byte // основной sitemap.xml
DomainSitemaps map[string][]byte // host → sitemap XML
// ...
}
func (nv *NoteViews) GetByRoute(host, path string) *NoteView
func (nv *NoteViews) RegisterNoteRoutes(note *NoteView)
func (nv *NoteViews) CustomDomains() []string
Ключевые файлы
| Файл | Что делает |
|---|---|
internal/model/note_routes.go |
ParsedRoute, ParseRoute, NormalizeDomain, ExtractHost, ExtractRoutes |
internal/model/note.go |
Routes в NoteView; RouteMap, DomainSitemaps в NoteViews; RegisterNoteRoutes, GetByRoute, CustomDomains |
internal/mdloader/loader.go |
Вызов ExtractRoutes() после патчей (строка ~607) |
internal/noteloader/loader.go |
Генерация DomainSitemaps после основного sitemap |
internal/case/rendernotepage/resolve.go |
resolveNote() — domain-aware lookup |
internal/case/rendernotepage/endpoint.go |
buildOGTags() / ogURLForNote() / findRouteForHost() |
internal/sitemap/sitemap.go |
Generate() + GenerateForDomain() |
cmd/server/main.go |
handleSitemap() — роутинг по Host заголовку |