feat: journal:fix-headings and the settings API

- classes.FixStoredHeadings converts stored H1 in post content (every
  locale) and category pillar text, in one transaction with a RecordSaved
  and a site cache purge per changed record
- journal:fix-headings, a dry run unless --apply, refuses with exit code 1
  while the H1 setting is off
- GET/PUT /_journal/api/v1/settings and POST /fix-headings {dry_run} for
  golem15.journal.manage_settings; 409 with the refusal while the setting
  is off
This commit is contained in:
Jakub Zych
2026-11-04 10:04:27 +01:00
parent faaa9b0f55
commit 5210b64e0f
8 changed files with 746 additions and 0 deletions

66
console/fix_headings.go Normal file
View File

@@ -0,0 +1,66 @@
package console
import (
"context"
"errors"
"fmt"
"git.golem15.com/golem15/sm-journal-plugin/classes"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
)
// FixHeadings is journal:fix-headings: it converts H1 to H2 in stored
// Journal rich text (post content in every locale and category pillar text)
// while the Journal H1 setting is on. It is a dry run unless --apply is
// given; with the setting off it refuses with exit code 1.
func FixHeadings(app *backpack.App) bonfire.Command {
return bonfire.Command{
Name: "journal:fix-headings",
Description: "Convert H1 headings to H2 in stored post and category content",
Long: `Finds every H1 heading in stored Journal content: post content and
content_html in the default locale and in every translation, and the pillar
text of every category. Switching the Journal H1 setting (Settings › Journal,
Headings and links) on never rewrites stored content; this command does.
By default it only counts ("Would convert N H1 headings in N posts and N
categories. Run with --apply to save."). With --apply it converts them in
one transaction, fires a record event per changed post and category (so an
SEO or search plugin hears about it) and queues the site cache purge.
While the H1 setting is off the command refuses and exits with code 1. The
MCP tool journal_fix_headings runs the same conversion.`,
Flags: []bonfire.Flag{
{Name: "apply", Description: "Save the converted content (default: dry run)", Bare: true},
{Name: "dry-run", Description: "Only count the H1 headings (the default)", Bare: true},
},
Examples: []bonfire.Example{
{Command: "journal:fix-headings", Description: "Count the H1 headings that would be converted"},
{Command: "journal:fix-headings --apply", Description: "Convert them and save"},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
db := mustDB(app)
if db == nil {
return errors.New("journal: database is not configured")
}
if !classes.EffectiveH1ToH2(ctx, db, appConfig(app)) {
out.Error(classes.H1ConversionOffText)
return bonfire.Reported(classes.ErrH1ConversionOff)
}
apply := flagOn(in, "apply") && !flagOn(in, "dry-run")
res, err := classes.FixStoredHeadings(ctx, app, db, apply)
if err != nil {
return err
}
switch {
case res.Headings == 0:
out.Info("No H1 headings found in post or category content.")
case apply:
out.Success(fmt.Sprintf("Converted %d H1 headings in %d posts and %d categories.", res.Headings, res.Posts, res.Categories))
default:
out.Info(fmt.Sprintf("Would convert %d H1 headings in %d posts and %d categories. Run with --apply to save.", res.Headings, res.Posts, res.Categories))
}
return nil
},
}
}