feat: contract.Rebuilder asks for a debounced rebuild
The sitemap publishes classes.Listing as contract.Rebuilder too, so a plugin whose Filter reads data of its own can queue the debounced rebuild a record event queues when that data changes.
This commit is contained in:
@@ -42,7 +42,7 @@ v0.1 served one `/sitemap.xml` in the WinterCMS byte format from a definition of
|
||||
- **HTML sitemap API.** `GET /_sitemap/api/v1/html` and `/_sitemap/api/v1/html/{group}` answer the HTML sitemap's groups and pages from the same indexable set as the XML sitemaps (D-34); the site's layer renders them.
|
||||
- **Settings singleton.** One tile in the CMS settings category: Files (site address, file size, the files table), Content types (per type: include, file name, HTML sitemap heading), Additional addresses, Images and News, IndexNow and HTML sitemap.
|
||||
- **Management API and MCP.** `GET`/`PUT /_sitemap/api/v1/settings` and `POST /_sitemap/api/v1/rebuild`, and the MCP tools `sitemap_get_settings`, `sitemap_update_settings` and `sitemap_rebuild` (D-10).
|
||||
- **Contract.** `contract.Filter` for other plugins to implement and `contract.Listing` for other plugins to read (D-95).
|
||||
- **Contract.** `contract.Filter` for other plugins to implement, `contract.Listing` for other plugins to read and `contract.Rebuilder` for other plugins to ask for a rebuild (D-95).
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -202,6 +202,10 @@ The plugin publishes `classes.Listing` as `contract.Listing`:
|
||||
|
||||
`Settings` also answers the HTML sitemap settings (`HTMLEnabled`, `HTMLPaths` by locale, `HTMLPageSize`) and the IndexNow key file name (`KeyFile`, `<key>.txt`, empty while IndexNow is off) and the content types switched off (`TypesOff`, whose addresses are left out with `ReasonType`), so the SEO plugin and the layer read them without importing the sitemap's internals.
|
||||
|
||||
### Rebuilder
|
||||
|
||||
The plugin also publishes `classes.Listing` as `contract.Rebuilder`. `RequestRebuild` queues one debounced rebuild, the same one a record event queues (a request inside the debounce window joins it), so a plugin whose `Filter` reads data of its own asks for a rebuild when that data changes; the SEO plugin does it when the frontend reports a changed set of noindex paths. A plugin that finds no `contract.Rebuilder` (an older sitemap plugin) leaves the change to the next scheduled rebuild.
|
||||
|
||||
### Admin
|
||||
|
||||
The Settings page has one Sitemaps tile (icon `network`, category CMS, permission `golem15.sitemap.access_definitions`). It opens the singleton controller `golem15.sitemap.settings`:
|
||||
|
||||
@@ -20,7 +20,10 @@ type Listing struct {
|
||||
App *backpack.App
|
||||
}
|
||||
|
||||
var _ contract.Listing = Listing{}
|
||||
var (
|
||||
_ contract.Listing = Listing{}
|
||||
_ contract.Rebuilder = Listing{}
|
||||
)
|
||||
|
||||
// The page sizes of Entries.
|
||||
const (
|
||||
@@ -39,6 +42,17 @@ func (l Listing) db(ctx context.Context) (*gorm.DB, error) {
|
||||
return db.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
// RequestRebuild queues one debounced rebuild through
|
||||
// RequestDebouncedRebuild, the same one a record event queues
|
||||
// (contract.Rebuilder).
|
||||
func (l Listing) RequestRebuild(ctx context.Context) error {
|
||||
db, err := l.db(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return RequestDebouncedRebuild(ctx, l.App, db)
|
||||
}
|
||||
|
||||
// Settings answers the site address, the index address, the default
|
||||
// locale and the groups with listed addresses (by key), with their titles
|
||||
// (a content type's saved HTML sitemap headings, else the titles of the
|
||||
|
||||
@@ -79,6 +79,18 @@ type Filter interface {
|
||||
Groups(ctx context.Context) ([]Group, error)
|
||||
}
|
||||
|
||||
// Rebuilder asks the sitemap for a rebuild. RequestRebuild queues one
|
||||
// debounced rebuild, the same one a record event queues: a request inside
|
||||
// the debounce window joins the rebuild already queued. Another plugin
|
||||
// calls it when something its Filter reads has changed (the SEO plugin's
|
||||
// noindex paths). The sitemap plugin publishes it with
|
||||
// app.Publish[contract.Rebuilder] in its Register; a plugin that finds none
|
||||
// published (an older sitemap plugin) leaves the change to the next
|
||||
// scheduled rebuild.
|
||||
type Rebuilder interface {
|
||||
RequestRebuild(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Listing is what the sitemap publishes for other plugins: its settings and
|
||||
// the listed addresses of each group. The sitemap plugin publishes it with
|
||||
// app.Publish[contract.Listing] in its Register.
|
||||
|
||||
@@ -48,11 +48,14 @@ func (p *Plugin) ID() string { return "golem15.sitemap" }
|
||||
// from a named plugin, and a Filter through the service registry.
|
||||
func (p *Plugin) Requires() []string { return nil }
|
||||
|
||||
// Register publishes the sitemap's contract.Listing for other plugins
|
||||
// (D-95).
|
||||
// Register publishes the sitemap's contract.Listing and contract.Rebuilder
|
||||
// for other plugins (D-95).
|
||||
func (p *Plugin) Register(app *backpack.App) error {
|
||||
p.app = app
|
||||
return app.Publish[contract.Listing](classes.Listing{App: app})
|
||||
if err := app.Publish[contract.Listing](classes.Listing{App: app}); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.Publish[contract.Rebuilder](classes.Listing{App: app})
|
||||
}
|
||||
|
||||
// Boot keeps the booted application for the routes, jobs and commands and
|
||||
|
||||
@@ -856,4 +856,25 @@ func TestListing(t *testing.T) {
|
||||
if err != nil || none.Total != 0 || len(none.Entries) != 0 || none.Next != "" {
|
||||
t.Fatalf("unknown group = %+v %v", none, err)
|
||||
}
|
||||
// contract.Rebuilder (P33-59) queues the debounced rebuild a record
|
||||
// event queues, once per debounce window.
|
||||
rb, ok := e.app.Lookup[contract.Rebuilder]()
|
||||
if !ok {
|
||||
t.Fatal("no contract.Rebuilder")
|
||||
}
|
||||
for range 2 {
|
||||
if err := rb.RequestRebuild(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var pending *time.Time
|
||||
if err := e.gdb.Raw(`SELECT rebuild_pending_until FROM golem15_sitemap_settings WHERE id = 1`).Scan(&pending).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pending == nil {
|
||||
t.Fatal("RequestRebuild set no rebuild_pending_until")
|
||||
}
|
||||
if n := e.riverJobs(t, classes.RebuildKind); n != 1 {
|
||||
t.Fatalf("two requests queued %d rebuilds, want 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user