note_rendering
Рендеринг заметок
Обзор
Процесс рендеринга заметки — это цепочка обработчиков от HTTP-запроса до HTML-ответа. Система использует два набора заметок (latest/live), поддерживает paywall, кастомные layout'ы и версионность.
HTTP Request Pipeline
1. Точка входа: fasthttp server
cmd/server/main.go:handler() (строка 1846) — основной обработчик всех HTTP-запросов.
HTTP Request → fasthttp handler → middleware chain → GraphQL | Redirect | Router
2. Middleware chain
Middlewares обрабатываются последовательно (строка 1857-1860). Если middleware вернёт true — запрос считается обработанным:
| Порядок | Middleware | Что делает |
|---|---|---|
| 1 | handleRobotsTxt |
/robots.txt |
| 2 | handleRSSFeed |
*.rss.xml |
| 3 | handleCors |
CORS headers |
| 4 | handleDebugAPI |
Debug endpoints |
| 5 | gitAPI.HandleRequest |
Git operations |
| 6 | handleAdminAssets |
Admin JS auth check |
| 7 | assets handler | /assets/* |
| 8 | handlePurchaseTokens |
Purchase token processing |
| 9 | signinbytgauthtoken |
Telegram auth |
| 10 | TgBots.ProcessWebhookRequest |
Telegram webhooks |
3. GraphQL / Redirect / Router
Если middleware не обработал запрос:
- GraphQL handler (
/graphql) — строка 1886 - Redirect manager — строка 1890
- Router с зарегистрированными endpoints — строка 1897
Если роутер не обработал → падает в catch-all endpoint rendernotepage.
Endpoint: rendernotepage
Точка входа: internal/case/rendernotepage/endpoint.go:Handle()
Фаза 1: Извлечение параметров
request := Request{
Path: string(req.Req.URI().Path()),
Version: string(req.Req.QueryArgs().Peek("version")),
Referrer: string(req.Req.Request.Header.Peek("Referer")),
UserToken: token,
}
Фаза 2: Business logic — Resolve()
internal/case/rendernotepage/resolve.go:Resolve()
Определение версии:
// Админы по умолчанию видят latest, пользователи — live
isAdmin := request.UserToken.IsAdmin()
isLatest := request.Version == "latest" || (isAdmin && request.Version == "")
if isLatest {
notes = env.LatestNoteViews()
} else {
notes = env.LiveNoteViews()
}
Поиск заметки:
note := notes.GetByPath(path) // Прямой lookup в map по пути
Access control:
if !note.Free && request.UserToken == nil {
return &PaywallError{Message: "Need auth"}
}
hasAccess, err := env.CanReadNote(ctx, note)
if !hasAccess {
return &PaywallError{Message: "Need subscription"}
}
Response:
response := Response{
Title: formatTitle(note.Title, env.SiteTitleTemplate()),
Note: note,
Notes: notes,
...
}
Фаза 3: Rendering
endpoint.go:Handle() — выбор режима рендера:
1. Redirect note (строка 63):
if resp.Note.Redirect != nil {
ctx.Response.Header.Set("Location", *resp.Note.Redirect)
ctx.SetStatusCode(http.StatusFound)
}
2. Onboarding (строка 69) — если нет заметок:
if resp.OnboardingMode {
return renderlayout.Handle(req, layoutParams, func() {
WriteOnboarding(ctx, resp)
})
}
3. Paywall (строка 79):
var paywallErr *PaywallError
if errors.As(err, &paywallErr) {
return renderlayout.Handle(req, layoutParams, func() {
WritePayWall(ctx, resp, paywallErr)
})
}
4. Turbo response (строка 97) — только HTML контент без layout:
if turbo := len(ctx.Request.Header.Peek("X-Turbo")) > 0; turbo {
WriteTurboNote(ctx, resp)
}
5. Custom layout (строка 109) — Jet template engine:
if layout := resp.Note.Layout; layout != "" {
layout := env.Layouts().Map["/"+layoutName]
vars := jet.VarMap{
"note": templateviews.NewNote(resp.Note),
"nvs": templateviews.NewNVS(resp.Notes, resp.DefaultVersion),
"title": resp.Title,
}
layout.View.Execute(ctx, vars, resp)
}
6. Standard layout (строка 120) — quicktemplate:
return renderlayout.Handle(req, layoutParams, func() {
WriteNote(ctx, resp) // quicktemplate генерирует HTML
})
NoteViews: загрузка и кеширование
Заметки загружаются в память при старте сервера и хранятся в двух версиях.
Loaders
cmd/server/main.go (строка 360-361):
a.liveNoteLoader = noteloader.New("live", makeLiveNoteLoaderWrapper(a), config)
a.latestNoteLoader = noteloader.New("latest", makeLatestNoteLoaderWrapper(a), config)
Загрузка: a.loadAllNotes(ctx, options) (строка 720)
Структура NoteView
Key fields (internal/model/note.go):
| Поле | Тип | Описание |
|---|---|---|
Path |
string |
URL путь заметки (например /blog/post) |
PathID |
int64 |
ID пути в БД |
VersionID |
int64 |
ID версии заметки |
Title |
string |
Заголовок |
Permalink |
string |
URL для ссылок |
Content |
[]byte |
Исходный markdown |
HTML |
string |
Рендеренный HTML |
Description |
*string |
Описание для meta tags |
CreatedAt |
time.Time |
Дата создания |
Free |
bool |
Доступна без подписки |
Redirect |
*string |
URL для редиректа |
RawMeta |
map[string]interface{} |
Frontmatter |
ResolvedLinks |
map[string]string |
Wikilinks → resolved URLs |
SubgraphNames |
[]string |
Список подграфов |
FirstImage |
*string |
Первое изображение для OG |
Layout |
string |
Кастомный layout |
Slug |
string |
URL slug |
RSSTitle |
string |
Заголовок для RSS |
RSSDescription |
string |
Описание для RSS |
Парсинг Markdown
internal/noteloader/ использует internal/mdloader/:
Goldmark extensions:
goldmark-meta— frontmatter parsinggoldmark-wikilink—[[wikilinks]]
Pipeline:
Markdown → Goldmark Parser → AST → NoteView
↓
ResolvedLinks map
AST хранится в NoteView.ast и используется для:
- Генерации HTML
- Извлечения ссылок для RSS
- Поиска по контенту
Разрешение ссылок:
wikilinks → notes.GetByPath(target) → ResolvedLinks[target] = note.Permalink
Title template
resolve.go:formatTitle() (строка 303):
func formatTitle(noteTitle, template string) string {
return fmt.Sprintf(template, noteTitle)
}
Template берётся из конфига site_title_template (по умолчанию "%s").
Пример:
note.Title = "My Post"
template = "%s | My Blog"
→ result = "My Post | My Blog"
Not Found (404)
Если заметка не найдена:
if note := notes.GetByPath(path); note == nil {
return &response, ErrNotFound
}
Обработка в endpoint.go (строка 87):
if errors.Is(err, ErrNotFound) {
ctx.SetStatusCode(http.StatusNotFound)
return render404.Handle(req)
}
User tracking
Когда пользователь открывает заметку, записывается:
user_note_views— каждое открытиеuser_note_daily_views— счётчик за день (max 100)
Запись происходит асинхронно (строка 245):
go func() {
bgCtx := context.Background()
env.RecordUserNoteView(bgCtx, userID, note, referrerVersionID)
}()
OG Tags
Open Graph tags генерируются в endpoint.go (строка 50):
layoutParams.OGTags = map[string]string{
"og:url": env.PublicURL() + resp.Note.Permalink,
"og:type": "article",
}
if resp.Note.FirstImage != nil {
layoutParams.OGTags["og:image"] = assetReplace.URL
}
Кеширование
NoteViews — хранятся в памяти, перезагружаются при:
PrepareLiveNotes()/PrepareLatestNotes()— после сохранения заметок- Истечении presigned URLs (строка 490-504 в
main.go)
Asset URLs — presigned URLs с TTL, обновляются автоматически перед истечением.