package console import ( "context" "encoding/json" "errors" "fmt" "os" "strings" "git.golem15.com/golem15/sm-sitemap-plugin/models" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "gorm.io/gorm" ) // Import is sitemap:import FILE [--append]: it reads a JSON array of URL // entries ({"url": "/path", "lastmod": "2026-10-28"}; lastmod optional) into // the settings' additional addresses, in one transaction. Without --append // the imported addresses replace the additional addresses; with --append // they are added after them, an address already listed is skipped. An // entry whose url is not a path or an http(s) URL (models.AllowedAddress), // or whose lastmod is not a date, stops the import before anything is // written. The Phase 17 keys changefreq and priority are ignored, and // --theme is accepted for the PHP command's sake. The files follow at the // next rebuild (sitemap:rebuild). func Import(app *backpack.App) bonfire.Command { return bonfire.Command{ Name: "sitemap:import", Description: "Import additional sitemap addresses from a JSON manifest file", Long: `Reads a JSON array of URL entries ({"url": "/path", "lastmod": "2026-10-28"}; lastmod is optional) and puts them into the sitemap's additional addresses (Settings > Sitemaps > Additional addresses), in one transaction. Every entry needs a url that is a path or an http(s) URL; one bad entry stops the import before anything is written. The old changefreq and priority keys are ignored: the sitemaps no longer write them. By default the import replaces the additional addresses. --append adds the addresses after them and skips those already listed. --theme is accepted for the PHP command's sake and does nothing. The files change at the next rebuild (sitemap:rebuild, a settings save or the nightly run). It ends with "Imported N addresses into the sitemap's additional addresses."`, Args: []bonfire.Arg{ {Name: "file", Description: "Path to the JSON manifest (relative to the working directory or absolute)", Required: true}, }, Flags: []bonfire.Flag{ {Name: "theme", Description: "Accepted for the PHP command; ignored"}, {Name: "append", Description: "Add the addresses instead of replacing the additional addresses", Bare: true}, }, Examples: []bonfire.Example{ {Command: "sitemap:import urls.json", Description: "Replace the additional addresses with the manifest's addresses"}, {Command: "sitemap:import urls.json --append", Description: "Add the addresses and keep the existing ones"}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { file, _ := in.Argument("file") db, done, err := lookupDB(ctx, app) if err != nil { return err } defer done() return ImportFile(ctx, db, file, flagValue(in, "theme"), flagOn(in, "append"), out) }, } } // ImportFile runs sitemap:import against db. func ImportFile(ctx context.Context, db *gorm.DB, file, theme string, appendMode bool, out bonfire.Output) error { raw, err := os.ReadFile(file) if err != nil { if errors.Is(err, os.ErrNotExist) { return fmt.Errorf("File not found: %s", file) } return fmt.Errorf("Cannot read %s: %w", file, err) } entries, labels, ok := models.LegacyEntries(raw) if !ok { return errors.New("Invalid JSON: expected an array of URL entries") } rows := make([]models.ExtraURL, 0, len(entries)) for i, entry := range entries { var fields map[string]json.RawMessage _ = json.Unmarshal(entry, &fields) url, err := models.DecodeText(fields["url"]) if fields["url"] == nil || err != nil || strings.TrimSpace(models.Text(url)) == "" { return fmt.Errorf("Entry %s is missing a 'url' field", labels[i]) } u := strings.TrimSpace(models.Text(url)) if !models.AllowedAddress(u) { return fmt.Errorf("Entry %s has a url that is not a path or an http(s) URL", labels[i]) } row := models.ExtraURL{URL: u} if raw, ok := fields["lastmod"]; ok { mod, err := models.DecodeText(raw) if err != nil || (strings.TrimSpace(models.Text(mod)) != "" && !models.ValidDate(models.Text(mod))) { return fmt.Errorf("Entry %s has a lastmod that is not a date (YYYY-MM-DD)", labels[i]) } row.LastMod = strings.TrimSpace(models.Text(mod)) } rows = append(rows, row) } var kept, added int err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { s, err := loadSettingsForUpdate(tx) if err != nil { return err } next := []models.ExtraURL{} seen := map[string]bool{} if appendMode { for _, e := range s.Extra() { next = append(next, e) seen[e.URL] = true } kept = len(next) } for _, r := range rows { if seen[r.URL] { continue } seen[r.URL] = true next = append(next, r) added++ } return saveExtra(tx, next) }) if err != nil { return err } out.Success(fmt.Sprintf("Imported %d addresses into the sitemap's additional addresses.", added)) if kept > 0 { out.Info(fmt.Sprintf("Kept %d existing addresses.", kept)) } if theme != "" { out.Warning(fmt.Sprintf("Theme '%s' was given; the sitemap has one set of additional addresses, so the addresses were imported there.", theme)) } out.Info("Run sitemap:rebuild (or save the sitemap settings) to update the files now.") return nil }