feat(15-03): add journal media upload, RSS, and off-by-default Typesense gate

- POST /_journal/api/v1/media/upload requires access_posts and stays under journal/
- Anonymous GET rss is RSS 2.0 honoring rss_* settings; unpublished posts are omitted
- beachcomber Gate reads search_use_typesense (default false) so a fresh save makes no Typesense HTTP

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-06 19:03:35 +02:00
parent 3a1dda45ec
commit bdd1c1be61
12 changed files with 748 additions and 21 deletions

View File

@@ -77,11 +77,19 @@ func Index(app *backpack.App) http.HandlerFunc {
search = ""
}
if search != "" {
like := "%" + escapeLike(search) + "%"
q = q.Where(
"(title ILIKE ? ESCAPE '\\' OR slug ILIKE ? ESCAPE '\\' OR excerpt ILIKE ? ESCAPE '\\' OR content ILIKE ? ESCAPE '\\')",
like, like, like, like,
)
if ids, used := searchWithEngine(app, r, db.WithContext(r.Context()), search, publishedOnly, page, perPage); used {
if len(ids) == 0 {
q = q.Where("1 = 0")
} else {
q = q.Where("id IN ?", ids)
}
} else {
like := "%" + escapeLike(search) + "%"
q = q.Where(
"(title ILIKE ? ESCAPE '\\' OR slug ILIKE ? ESCAPE '\\' OR excerpt ILIKE ? ESCAPE '\\' OR content ILIKE ? ESCAPE '\\')",
like, like, like, like,
)
}
}
var total int64
@@ -181,15 +189,6 @@ func Tags(app *backpack.App) http.HandlerFunc {
}
}
// RSS is GET /_journal/api/v1/rss. Task 3 fills the PHP RSS contract.
func RSS(app *backpack.App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel></channel></rss>`))
}
}
// Store is POST /_journal/api/v1/posts. Field-by-field create; never lagoon.Fill.
func Store(app *backpack.App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {