fix: resolve translated post slugs (D-58)

GET /_journal/api/v1/posts/{slug} now looks the slug up in translate's
slug index for the request locale after the default-locale slug and
before the numeric id. TestTranslatedSlug covers the translated slug,
the default slug, the id and an unknown slug.
This commit is contained in:
2026-10-26 22:46:49 +01:00
parent e17cebd761
commit a598a7c5e7
4 changed files with 105 additions and 3 deletions

View File

@@ -128,7 +128,7 @@ func Show(app *backpack.App) http.HandlerFunc {
}
db = db.WithContext(r.Context())
slug := strings.TrimSpace(r.PathValue("slug"))
post, err := findPostBySlugOrID(db, slug)
post, err := findPostBySlugOrID(db, slug, requestLocale(r))
if err != nil || post == nil {
writeAPIError(w, http.StatusNotFound, errPostNotFound)
return

View File

@@ -16,8 +16,10 @@ import (
"git.golem15.com/golem15/sm-journal-plugin/classes"
"git.golem15.com/golem15/sm-journal-plugin/models"
translate "git.golem15.com/golem15/sm-translate-plugin/classes"
translatemodels "git.golem15.com/golem15/sm-translate-plugin/models"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/towel"
"gorm.io/gorm"
)
@@ -52,12 +54,29 @@ func ctypeDigit(s string) bool {
return true
}
func findPostBySlugOrID(db *gorm.DB, slug string) (*models.Post, error) {
// findPostBySlugOrID finds a post by its default-locale slug, then by its
// slug in locale (the request locale, D-58: translate's slug index), then
// by numeric id. Drafts are found too; the caller decides who may see them.
func findPostBySlugOrID(db *gorm.DB, slug, locale string) (*models.Post, error) {
q := postQuery(db)
var post models.Post
if err := q.Where("slug = ?", slug).Take(&post).Error; err == nil {
return &post, nil
}
if locale != "" && slug != "" {
var ids []string
err := db.Session(&gorm.Session{NewDB: true, Context: db.Statement.Context}).
Table(translatemodels.Index{}.TableName()).
Where("model_type = ? AND locale = ? AND item = ? AND value = ?", models.Post{}.MorphName(), locale, "slug", slug).
Order("id ASC").
Pluck("model_id", &ids).Error
if err == nil && len(ids) > 0 {
var translated models.Post
if err := postQuery(db).Where("CAST(id AS TEXT) IN ?", ids).Order("id ASC").Take(&translated).Error; err == nil {
return &translated, nil
}
}
}
if ctypeDigit(slug) {
id, err := strconv.ParseUint(slug, 10, 64)
if err == nil && id > 0 {
@@ -780,3 +799,21 @@ func syncTranslations(ctx context.Context, db *gorm.DB, post *models.Post, raw a
}
return nil
}
// requestLocale is the request's locale (towel, set by the router from the
// locale resolver or Accept-Language) as a lower-case language code: the
// first language of an Accept-Language list, without its region.
func requestLocale(r *http.Request) string {
if r == nil {
return ""
}
raw, _ := towel.Locale(r.Context())
raw = strings.TrimSpace(raw)
if i := strings.IndexAny(raw, ",;"); i >= 0 {
raw = raw[:i]
}
if i := strings.IndexAny(raw, "-_"); i >= 0 {
raw = raw[:i]
}
return strings.ToLower(strings.TrimSpace(raw))
}