From fb52f3885088d5230339e086eced5100eb4b1cc5 Mon Sep 17 00:00:00 2001 From: Sam Douglas Date: Thu, 11 Dec 2025 09:30:27 +1300 Subject: [PATCH 1/5] add published date to page model --- kibble/api/bios.go | 2 ++ kibble/models/pages.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/kibble/api/bios.go b/kibble/api/bios.go index e3121736..b074340a 100644 --- a/kibble/api/bios.go +++ b/kibble/api/bios.go @@ -70,6 +70,7 @@ func (p pageV1) mapToModel(serviceConfig models.ServiceConfig, itemIndex models. }, PageCollections: make([]models.PageCollection, 0), CustomFields: p.CustomFields, + PublishedDate: utils.ParseTimeFromString(p.PublishedDate), } page.Seo = models.Seo{ @@ -132,6 +133,7 @@ type pageV1 struct { Title string `json:"title"` URL string `json:"url"` CustomFields map[string]interface{} `json:"custom"` + PublishedDate string `json:"published_date"` } type filmSummary struct { diff --git a/kibble/models/pages.go b/kibble/models/pages.go index e3e4972a..2421ae2e 100644 --- a/kibble/models/pages.go +++ b/kibble/models/pages.go @@ -14,6 +14,8 @@ package models +import "time" + // PageCollection - part of a page type PageCollection struct { ID int @@ -42,6 +44,7 @@ type Page struct { URL string CustomFields CustomFields Plans []Plan + PublishedDate time.Time } // Pages - From 5a3e0d84aabb1da1cd6edf99a29fc490b4de88bb Mon Sep 17 00:00:00 2001 From: Sam Douglas Date: Thu, 11 Dec 2025 09:31:07 +1300 Subject: [PATCH 2/5] add options for PageIndex datasource: sorting and page type filtering --- kibble/datastore/page_index_datasource.go | 107 ++++++++++++++++++++-- kibble/models/route_registy.go | 24 ++--- 2 files changed, 112 insertions(+), 19 deletions(-) diff --git a/kibble/datastore/page_index_datasource.go b/kibble/datastore/page_index_datasource.go index 8420956a..720a50b6 100644 --- a/kibble/datastore/page_index_datasource.go +++ b/kibble/datastore/page_index_datasource.go @@ -15,8 +15,10 @@ package datastore import ( + "encoding/json" "fmt" "reflect" + "sort" "strconv" "strings" @@ -38,6 +40,21 @@ var indexArgs = []models.RouteArgument{ // PageIndexDataSource - a list of all Pages type PageIndexDataSource struct{} +type pageIndexDataSourceOptions struct { + PageTypes []string `json:"pageTypes"` + SortBy []string `json:"sortBy"` +} + +func (opts pageIndexDataSourceOptions) IsAllowedPageType(pageType string) bool { + for _, pt := range opts.PageTypes { + if pt == pageType { + return true + } + } + + return len(opts.PageTypes) == 0 +} + // GetName - returns the name of the datasource func (ds *PageIndexDataSource) GetName() string { return "PageIndex" @@ -56,6 +73,26 @@ func (ds *PageIndexDataSource) GetEntityType() reflect.Type { // Iterator - return a list of all Pages, iteration of 1 func (ds *PageIndexDataSource) Iterator(ctx models.RenderContext, renderer models.Renderer) (errCount int) { + var options pageIndexDataSourceOptions + if len(ctx.Route.Options) > 0 { + err := json.Unmarshal(ctx.Route.Options, &options) + if err != nil { + panic(fmt.Errorf("unable to parse datasource options: %w", err)) + } + } + + pages := make(models.Pages, 0, len(ctx.Site.Pages)) + for _, page := range ctx.Site.Pages { + if !options.IsAllowedPageType(page.PageType) { + continue + } + pages = append(pages, page) + } + + if len(options.SortBy) > 0 { + sortPages(pages, ParseSortKeys(options.SortBy)) + } + // rule for page 1 if ctx.Route.PageSize > 0 { @@ -65,7 +102,7 @@ func (ds *PageIndexDataSource) Iterator(ctx models.RenderContext, renderer model ctx.Route.Pagination = models.Pagination{ Index: 1, - Total: (len(ctx.Site.Pages) / ctx.Route.PageSize) + 1, + Total: (len(pages) / ctx.Route.PageSize) + 1, Size: ctx.Route.PageSize, } @@ -93,13 +130,13 @@ func (ds *PageIndexDataSource) Iterator(ctx models.RenderContext, renderer model startIndex := pi * ctx.Route.PageSize endIndex := ((pi * ctx.Route.PageSize) + ctx.Route.PageSize) - 1 - if endIndex >= len(ctx.Site.Pages) { - endIndex = len(ctx.Site.Pages) - 1 + if endIndex >= len(pages) { + endIndex = len(pages) - 1 } clonedPages := make([]*models.Page, endIndex-startIndex+1) for i := startIndex; i <= endIndex; i++ { - clonedPages[i-startIndex] = transformPage(ctx.Site.Pages[i]) + clonedPages[i-startIndex] = transformPage(pages[i]) } vars := make(jet.VarMap) @@ -112,12 +149,12 @@ func (ds *PageIndexDataSource) Iterator(ctx models.RenderContext, renderer model ctx.Route.Pagination = models.Pagination{ Index: 1, - Total: len(ctx.Site.Pages), - Size: len(ctx.Site.Pages), + Total: len(pages), + Size: len(pages), } - clonedPages := make([]*models.Page, len(ctx.Site.Pages)) - for i, f := range ctx.Site.Pages { + clonedPages := make([]*models.Page, len(pages)) + for i, f := range pages { clonedPages[i] = transformPage(f) } @@ -155,3 +192,57 @@ func transformPage(f models.Page) *models.Page { return &f } + +type sortKey struct { + Field string + Desc bool +} + +func ParseSortKeys(raw []string) []sortKey { + keys := make([]sortKey, 0, len(raw)) + for _, s := range raw { + parts := strings.SplitN(s, ":", 2) + key := sortKey{Field: parts[0]} + if len(parts) == 2 && strings.EqualFold(parts[1], "desc") { + key.Desc = true + } + keys = append(keys, key) + } + return keys +} + +func sortPages(pages models.Pages, keys []sortKey) { + // Apply from last to first so earlier keys win (like SQL ORDER BY) + for i := len(keys) - 1; i >= 0; i-- { + key := keys[i] + + sort.SliceStable(pages, func(i, j int) bool { + pi, pj := pages[i], pages[j] + + switch key.Field { + case "published_date": + // handle equal first so we don't randomly flip + if pi.PublishedDate.Equal(pj.PublishedDate) { + return false + } + if key.Desc { + return pi.PublishedDate.After(pj.PublishedDate) + } + return pi.PublishedDate.Before(pj.PublishedDate) + + case "title": + if pi.Title == pj.Title { + return false + } + if key.Desc { + return pi.Title > pj.Title + } + return pi.Title < pj.Title + + default: + // unknown field → no-op for this pass + return false + } + }) + } +} diff --git a/kibble/models/route_registy.go b/kibble/models/route_registy.go index c598e8a1..ee1bfbed 100644 --- a/kibble/models/route_registy.go +++ b/kibble/models/route_registy.go @@ -15,6 +15,7 @@ package models import ( + "encoding/json" "errors" "fmt" "reflect" @@ -22,17 +23,18 @@ import ( // Route - represents a route for rendering and type Route struct { - Name string `json:"name"` - URLPath string `json:"urlPath"` - TemplatePath string `json:"templatePath"` - PartialURLPath string `json:"partialUrlPath"` - PartialTemplatePath string `json:"partialTemplatePath"` - DataSource string `json:"datasource"` - ResolvedDataSource DataSource `json:"-"` - ResolvedEntityType reflect.Type `json:"-"` - PageSize int `json:"pageSize"` - Pagination Pagination `json:"-"` - DefaultLanguageOnly bool `json:"defaultLanguageOnly"` + Name string `json:"name"` + URLPath string `json:"urlPath"` + TemplatePath string `json:"templatePath"` + PartialURLPath string `json:"partialUrlPath"` + PartialTemplatePath string `json:"partialTemplatePath"` + DataSource string `json:"datasource"` + ResolvedDataSource DataSource `json:"-"` + ResolvedEntityType reflect.Type `json:"-"` + PageSize int `json:"pageSize"` + Pagination Pagination `json:"-"` + DefaultLanguageOnly bool `json:"defaultLanguageOnly"` + Options json.RawMessage `json:"options"` // extra options, used by the data source implementation. } // Pagination describes a single page of results From d0b9b139a577dc95c039fd54f4d004832da3105f Mon Sep 17 00:00:00 2001 From: Sam Douglas Date: Mon, 15 Dec 2025 11:53:09 +1300 Subject: [PATCH 3/5] Support having different url for first page of a page index e.g. /posts/ rather than /posts/1 for the index, then subsequent can be /posts/2 etc. --- kibble/datastore/page_index_datasource.go | 22 ++++++++++++++-------- kibble/models/route_registy.go | 1 + 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/kibble/datastore/page_index_datasource.go b/kibble/datastore/page_index_datasource.go index 720a50b6..765c102a 100644 --- a/kibble/datastore/page_index_datasource.go +++ b/kibble/datastore/page_index_datasource.go @@ -93,6 +93,17 @@ func (ds *PageIndexDataSource) Iterator(ctx models.RenderContext, renderer model sortPages(pages, ParseSortKeys(options.SortBy)) } + getPaginationUrl := func(pageIndex int) string { + var urlPath string + if len(ctx.Route.FirstPageURLPath) > 0 && pageIndex <= 1 { + urlPath = ctx.Route.FirstPageURLPath + } else { + urlPath = ctx.Route.URLPath + } + + return strings.Replace(urlPath, ":index", strconv.Itoa(pageIndex), 1) + } + // rule for page 1 if ctx.Route.PageSize > 0 { @@ -113,19 +124,14 @@ func (ds *PageIndexDataSource) Iterator(ctx models.RenderContext, renderer model ctx.Route.Pagination.PreviousURL = "" ctx.Route.Pagination.NextURL = "" - path := strings.Replace(ctx.Route.URLPath, ":index", - strconv.Itoa(ctx.Route.Pagination.Index), 1) + path := getPaginationUrl(ctx.Route.Pagination.Index) if pi > 0 { - ctx.Route.Pagination.PreviousURL = - strings.Replace(ctx.Route.URLPath, ":index", - strconv.Itoa(ctx.Route.Pagination.Index-1), 1) + ctx.Route.Pagination.PreviousURL = getPaginationUrl(ctx.Route.Pagination.Index - 1) } if pi < ctx.Route.Pagination.Total-1 { - ctx.Route.Pagination.NextURL = - strings.Replace(ctx.Route.URLPath, ":index", - strconv.Itoa(ctx.Route.Pagination.Index+1), 1) + ctx.Route.Pagination.NextURL = getPaginationUrl(ctx.Route.Pagination.Index + 1) } startIndex := pi * ctx.Route.PageSize diff --git a/kibble/models/route_registy.go b/kibble/models/route_registy.go index ee1bfbed..c00ce4bb 100644 --- a/kibble/models/route_registy.go +++ b/kibble/models/route_registy.go @@ -25,6 +25,7 @@ import ( type Route struct { Name string `json:"name"` URLPath string `json:"urlPath"` + FirstPageURLPath string `json:"firstPageUrlPath"` TemplatePath string `json:"templatePath"` PartialURLPath string `json:"partialUrlPath"` PartialTemplatePath string `json:"partialTemplatePath"` From f85cf4e5e3dfa9f27067b7992781782e193105f7 Mon Sep 17 00:00:00 2001 From: Sam Douglas Date: Mon, 15 Dec 2025 11:55:09 +1300 Subject: [PATCH 4/5] Warn rather than die if a wild page type appears the page data source supports a `:type` substitution, but currently it dies if there's a page type the template doesn't support. Now it just warns and keeps going. This should avoid failing site builds. --- kibble/datastore/page_datasource.go | 12 ++++++++++-- kibble/models/render.go | 1 + kibble/render/file_renderer.go | 5 +++++ kibble/test/inmemory_renderer.go | 5 +++++ kibble/test/inmemory_template_renderer.go | 4 ++++ kibble/test/mock_renderer.go | 4 ++++ 6 files changed, 29 insertions(+), 2 deletions(-) diff --git a/kibble/datastore/page_datasource.go b/kibble/datastore/page_datasource.go index 5eb95029..db094282 100644 --- a/kibble/datastore/page_datasource.go +++ b/kibble/datastore/page_datasource.go @@ -82,13 +82,21 @@ func (ds *PageDataSource) Iterator(ctx models.RenderContext, renderer models.Ren // don't render external pages if p.PageType != "external" { templatePath := strings.Replace(ctx.Route.TemplatePath, ":type", p.PageType, 1) - errCount += renderer.Render(templatePath, ds.GetRouteForEntity(ctx, &p), data) + if renderer.HasTemplate(templatePath) || !strings.Contains(ctx.Route.TemplatePath, ":type") { + errCount += renderer.Render(templatePath, ds.GetRouteForEntity(ctx, &p), data) + } else { + log.Warning("Template not found for page type '%s': %s", p.PageType, templatePath) + } } // now partial end points if ctx.Route.HasPartial() { templatePath := strings.Replace(ctx.Route.PartialTemplatePath, ":type", p.PageType, 1) - errCount += renderer.Render(templatePath, ds.GetPartialRouteForEntity(ctx, &p), data) + if renderer.HasTemplate(templatePath) || !strings.Contains(ctx.Route.PartialTemplatePath, ":type") { + errCount += renderer.Render(templatePath, ds.GetPartialRouteForEntity(ctx, &p), data) + } else { + log.Warning("Partial Template not found for page type '%s': %s", p.PageType, templatePath) + } } } diff --git a/kibble/models/render.go b/kibble/models/render.go index be4d0907..191cf345 100644 --- a/kibble/models/render.go +++ b/kibble/models/render.go @@ -30,4 +30,5 @@ type RenderContext struct { type Renderer interface { Initialise() Render(templatePath string, filePath string, data jet.VarMap) (errorCount int) + HasTemplate(templatePath string) bool } diff --git a/kibble/render/file_renderer.go b/kibble/render/file_renderer.go index 836d3952..95ac3d62 100644 --- a/kibble/render/file_renderer.go +++ b/kibble/render/file_renderer.go @@ -114,6 +114,11 @@ func (c FileRenderer) Render(templatePath string, filePath string, data jet.VarM return errorCount } +func (c FileRenderer) HasTemplate(templatePath string) bool { + _, err := c.view.GetTemplate(templatePath) + return err == nil +} + func writeFile(filename string, data []byte) error { file, err := os.Create(filename) if err != nil { diff --git a/kibble/test/inmemory_renderer.go b/kibble/test/inmemory_renderer.go index 2117cdca..4c589b59 100644 --- a/kibble/test/inmemory_renderer.go +++ b/kibble/test/inmemory_renderer.go @@ -104,3 +104,8 @@ func (c *InMemoryRenderer) Render(templatePath string, filePath string, data jet c.Results = append(c.Results, result) return } + +func (c *InMemoryRenderer) HasTemplate(templatePath string) bool { + _, err := c.View.GetTemplate(templatePath) + return err == nil +} diff --git a/kibble/test/inmemory_template_renderer.go b/kibble/test/inmemory_template_renderer.go index 80f6306e..cf387934 100644 --- a/kibble/test/inmemory_template_renderer.go +++ b/kibble/test/inmemory_template_renderer.go @@ -47,3 +47,7 @@ func (c *InMemoryTemplateRenderer) Render(templatePath string, filePath string, return } + +func (c *InMemoryTemplateRenderer) HasTemplate(templatePath string) bool { + return true +} diff --git a/kibble/test/mock_renderer.go b/kibble/test/mock_renderer.go index f4531aa5..79eb3dd8 100644 --- a/kibble/test/mock_renderer.go +++ b/kibble/test/mock_renderer.go @@ -40,3 +40,7 @@ func (c *MockRenderer) Render(templatePath string, filePath string, data jet.Var c.Data = data return } + +func (c *MockRenderer) HasTemplate(templatePath string) bool { + return true +} From e5f2c46e54f5f4c454814ef86850ce5fc92fbf28 Mon Sep 17 00:00:00 2001 From: Sam Douglas Date: Mon, 15 Dec 2025 16:02:57 +1300 Subject: [PATCH 5/5] Refactor pagination logic out of the PageIndex datasource Refactored into a reusable method on the RenderContext to handle much of the pagination and url generation logic. This allows a lot of code in the datasource to be tidied up, and also fixes some bugs like including the RoutePrefix on the pagination links --- kibble/datastore/page_index_datasource.go | 78 ++------------- kibble/models/paginator.go | 110 ++++++++++++++++++++++ kibble/models/route_registy.go | 24 +---- 3 files changed, 120 insertions(+), 92 deletions(-) create mode 100644 kibble/models/paginator.go diff --git a/kibble/datastore/page_index_datasource.go b/kibble/datastore/page_index_datasource.go index 765c102a..921da8c0 100644 --- a/kibble/datastore/page_index_datasource.go +++ b/kibble/datastore/page_index_datasource.go @@ -19,7 +19,6 @@ import ( "fmt" "reflect" "sort" - "strconv" "strings" "kibble/models" @@ -93,82 +92,21 @@ func (ds *PageIndexDataSource) Iterator(ctx models.RenderContext, renderer model sortPages(pages, ParseSortKeys(options.SortBy)) } - getPaginationUrl := func(pageIndex int) string { - var urlPath string - if len(ctx.Route.FirstPageURLPath) > 0 && pageIndex <= 1 { - urlPath = ctx.Route.FirstPageURLPath - } else { - urlPath = ctx.Route.URLPath - } - - return strings.Replace(urlPath, ":index", strconv.Itoa(pageIndex), 1) + if ctx.Route.PageSize > 0 && !strings.Contains(ctx.Route.URLPath, ":index") { + panic(fmt.Errorf("page route is missing an :index. Either add and index placeholder or remove the pageSize")) } - // rule for page 1 - if ctx.Route.PageSize > 0 { - - if !strings.Contains(ctx.Route.URLPath, ":index") { - panic(fmt.Errorf("Page route is missing an :index. Either add and index placeholder or remove the pageSize")) - } - - ctx.Route.Pagination = models.Pagination{ - Index: 1, - Total: (len(pages) / ctx.Route.PageSize) + 1, - Size: ctx.Route.PageSize, - } - - // page count - for pi := 0; pi < ctx.Route.Pagination.Total; pi++ { - - ctx.Route.Pagination.Index = pi + 1 - ctx.Route.Pagination.PreviousURL = "" - ctx.Route.Pagination.NextURL = "" - - path := getPaginationUrl(ctx.Route.Pagination.Index) - - if pi > 0 { - ctx.Route.Pagination.PreviousURL = getPaginationUrl(ctx.Route.Pagination.Index - 1) - } - - if pi < ctx.Route.Pagination.Total-1 { - ctx.Route.Pagination.NextURL = getPaginationUrl(ctx.Route.Pagination.Index + 1) - } - - startIndex := pi * ctx.Route.PageSize - endIndex := ((pi * ctx.Route.PageSize) + ctx.Route.PageSize) - 1 - if endIndex >= len(pages) { - endIndex = len(pages) - 1 - } - - clonedPages := make([]*models.Page, endIndex-startIndex+1) - for i := startIndex; i <= endIndex; i++ { - clonedPages[i-startIndex] = transformPage(pages[i]) - } - - vars := make(jet.VarMap) - vars.Set("pages", clonedPages) - vars.Set("pagination", ctx.Route.Pagination) - vars.Set("site", ctx.Site) - errCount += renderer.Render(ctx.Route.TemplatePath, ctx.RoutePrefix+path, vars) - } - } else { - - ctx.Route.Pagination = models.Pagination{ - Index: 1, - Total: len(pages), - Size: len(pages), - } - - clonedPages := make([]*models.Page, len(pages)) - for i, f := range pages { - clonedPages[i] = transformPage(f) + for _, pagination := range ctx.Paginate(len(pages)) { + clonedPages := make([]*models.Page, pagination.ItemCount) + for i := 0; i < pagination.ItemCount; i++ { + clonedPages[i] = transformPage(pages[pagination.ItemSliceStart+i]) } vars := make(jet.VarMap) vars.Set("pages", clonedPages) - vars.Set("pagination", ctx.Route.Pagination) + vars.Set("pagination", pagination) vars.Set("site", ctx.Site) - errCount += renderer.Render(ctx.Route.TemplatePath, ctx.RoutePrefix+ctx.Route.URLPath, vars) + errCount += renderer.Render(ctx.Route.TemplatePath, pagination.CurrentURL, vars) } return diff --git a/kibble/models/paginator.go b/kibble/models/paginator.go new file mode 100644 index 00000000..35e1780d --- /dev/null +++ b/kibble/models/paginator.go @@ -0,0 +1,110 @@ +package models + +import ( + "strconv" + "strings" +) + +type Paginator struct { + Route *Route + RoutePrefix string + TotalItems int +} + +func (ctx *RenderContext) Paginate(totalItems int) []Pagination { + return Paginator{ + RoutePrefix: ctx.RoutePrefix, + Route: ctx.Route, + TotalItems: totalItems, + }.GetAll() +} + +// Pagination describes a single page of results +type Pagination struct { + Index int // current page, 1-based index + Size int // nominal page size. Actual number of results may differ + Total int // Total pages + PreviousURL string // Prev page href, or blank + CurrentURL string // Current page href + NextURL string // Next page href, or blank + ItemSliceStart int // First item index (inclusive) for use with [:] slice operator + ItemSliceEnd int // Last item index (exclusive) for use with [:] slice operator + ItemCount int // Number of items on the page +} + +func (p Paginator) GetAll() []Pagination { + firstPage := p.GetPagination(1) + + results := make([]Pagination, firstPage.Total) + results[0] = firstPage + for i := 1; i < firstPage.Total; i++ { + results[i] = p.GetPagination(i + 1) + } + + return results +} + +func (p Paginator) GetPagination(currentPage int) Pagination { + totalItems := p.TotalItems + pageSize := p.Route.PageSize + if pageSize <= 0 { + // slightly weird edge case here: if there are no items the page size is 0 + pageSize = totalItems + } + + var totalPages int + if pageSize <= 0 || totalItems <= 0 { + // ensure that empty collections always render at least one page + totalPages = 1 + } else { + totalPages = (totalItems + pageSize - 1) / pageSize + } + + if currentPage < 1 { + currentPage = 1 + } else if currentPage > totalPages { + currentPage = totalPages + } + + pagination := Pagination{ + Index: currentPage, + Total: totalPages, + Size: pageSize, + CurrentURL: p.GetPaginationURLPath(currentPage), + PreviousURL: "", + NextURL: "", + ItemSliceStart: 0, + ItemSliceEnd: 0, + ItemCount: 0, + } + + // NOTE: follows Go slice semantics [start:end) + pagination.ItemSliceStart = (pagination.Index - 1) * pageSize + pagination.ItemSliceEnd = pagination.ItemSliceStart + pageSize + if pagination.ItemSliceEnd > totalItems { + pagination.ItemSliceEnd = totalItems + } + + pagination.ItemCount = pagination.ItemSliceEnd - pagination.ItemSliceStart + + if pagination.Index > 1 { + pagination.PreviousURL = p.GetPaginationURLPath(pagination.Index - 1) + } + + if pagination.Index < pagination.Total { + pagination.NextURL = p.GetPaginationURLPath(pagination.Index + 1) + } + + return pagination +} + +func (p Paginator) GetPaginationURLPath(pageIndex int) string { + var urlPath string + if len(p.Route.FirstPageURLPath) > 0 && pageIndex <= 1 { + urlPath = p.Route.FirstPageURLPath + } else { + urlPath = p.Route.URLPath + } + + return p.RoutePrefix + strings.Replace(urlPath, ":index", strconv.Itoa(pageIndex), 1) +} diff --git a/kibble/models/route_registy.go b/kibble/models/route_registy.go index c00ce4bb..7b335e9f 100644 --- a/kibble/models/route_registy.go +++ b/kibble/models/route_registy.go @@ -33,34 +33,14 @@ type Route struct { ResolvedDataSource DataSource `json:"-"` ResolvedEntityType reflect.Type `json:"-"` PageSize int `json:"pageSize"` - Pagination Pagination `json:"-"` DefaultLanguageOnly bool `json:"defaultLanguageOnly"` Options json.RawMessage `json:"options"` // extra options, used by the data source implementation. } -// Pagination describes a single page of results -type Pagination struct { - Index int - Size int - Total int - PreviousURL string - NextURL string -} - // Clone - create a copy of the route func (r *Route) Clone() *Route { - return &Route{ - Name: r.Name, - URLPath: r.URLPath, - TemplatePath: r.TemplatePath, - PartialURLPath: r.PartialURLPath, - PartialTemplatePath: r.PartialTemplatePath, - DataSource: r.DataSource, - ResolvedDataSource: r.ResolvedDataSource, - ResolvedEntityType: r.ResolvedEntityType, - PageSize: r.PageSize, - Pagination: r.Pagination, - } + clone := *r + return &clone } // HasPartial returns whether the route has partial path (url and template) definitions