feat: H1 conversion on the API, markdown and import paths

- the /_journal/api/v1 post writes convert default and translated content
  in the write transaction and answer an additive notices key
- journal:import-posts converts and prints the converted count
  (ImportFromDirConfig, ImportStats.H1Converted)
- a category's pillar text saved in the admin answers
  editor.h1_converted_category
- every GET /categories row reports content_h1_to_h2 (additive key)
This commit is contained in:
Jakub Zych
2026-11-04 09:51:47 +01:00
parent 305f94afd9
commit faaa9b0f55
11 changed files with 459 additions and 42 deletions

View File

@@ -193,7 +193,8 @@ func Categories(app *backpack.App) http.HandlerFunc {
return
}
includeEmpty := queryBool(r.URL.Query().Get("include_empty"))
data, err := serializeCategoryTree(db.WithContext(r.Context()), includeEmpty)
h1ToH2 := classes.EffectiveH1ToH2(r.Context(), db, appConfig(app))
data, err := serializeCategoryTree(db.WithContext(r.Context()), includeEmpty, h1ToH2)
if err != nil {
writeAPIError(w, http.StatusInternalServerError, "Internal server error")
return
@@ -250,11 +251,16 @@ func Store(app *backpack.App) http.HandlerFunc {
}
// D-92/D-102: the row, its associations and translations commit
// together, and RecordSaved fires only once they have.
var h1 *h1Conversion
err = lagoon.Transaction(r.Context(), db, func(ctx context.Context, tx *gorm.DB) error {
// D-50: while the H1 setting is on, H1 in the stored HTML
// becomes H2, in the same transaction as the write.
h1 = newH1Conversion(ctx, tx, app)
h1.convertPost(post)
if err := tx.Create(post).Error; err != nil {
return err
}
tags, err := applyPostAssocs(ctx, tx, post, body)
tags, err := applyPostAssocs(ctx, tx, post, body, h1)
if err != nil {
return err
}
@@ -272,7 +278,7 @@ func Store(app *backpack.App) http.HandlerFunc {
return
}
purgeSiteCache(r, app, post)
wire.WriteJSON(w, http.StatusCreated, map[string]any{"data": serializePost(post, db.WithContext(r.Context()))})
wire.WriteJSON(w, http.StatusCreated, h1.answer(r, app, map[string]any{"data": serializePost(post, db.WithContext(r.Context()))}))
}
}
@@ -318,11 +324,18 @@ func Update(app *backpack.App) http.HandlerFunc {
}
// D-92/D-102: a slug change commits with its translations and
// associations, or not at all, and RecordSaved follows the commit.
var h1 *h1Conversion
err = lagoon.Transaction(r.Context(), db, func(ctx context.Context, tx *gorm.DB) error {
// D-50: only a submitted content is converted; an update
// that does not write the body leaves stored H1 alone.
h1 = newH1Conversion(ctx, tx, app)
if _, has := body["content"]; has {
h1.convertPost(post)
}
if err := tx.Save(post).Error; err != nil {
return err
}
tags, err := applyPostAssocs(ctx, tx, post, body)
tags, err := applyPostAssocs(ctx, tx, post, body, h1)
if err != nil {
return err
}
@@ -340,7 +353,7 @@ func Update(app *backpack.App) http.HandlerFunc {
return
}
purgeSiteCache(r, app, post)
wire.WriteJSON(w, http.StatusOK, map[string]any{"data": serializePost(post, db.WithContext(r.Context()))})
wire.WriteJSON(w, http.StatusOK, h1.answer(r, app, map[string]any{"data": serializePost(post, db.WithContext(r.Context()))}))
}
}