Add page management to the structured editor

Pages can now be reordered, renamed, and have their metadata edited
without touching YAML.

- Up/down arrows next to each page in the /edit list to reorder.
- "Settings" button per page opens /edit/pages/{slug}/settings — a
  form covering name, slug, page width, desktop nav width, mobile
  header, hide desktop nav, center vertically.
- POST /edit/api/pages/{slug}/fields applies metadata changes
  surgically via the existing yaml.Node + atomic-save flow so
  columns/widgets/comments stay untouched.
- POST /edit/api/pages/{slug}/move?dir=up|down swaps adjacent pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
uhlwoogi
2026-04-30 17:18:52 +00:00
co-authored by Claude Opus 4.7
parent 8ba6dcb704
commit 7f36f30dad
6 changed files with 270 additions and 4 deletions
+53 -3
View File
@@ -10,9 +10,10 @@ import (
)
var (
adminIndexTemplate = mustParseTemplate("admin.html", "document.html", "footer.html")
adminPageTemplate = mustParseTemplate("admin-page.html", "document.html", "footer.html")
adminWidgetTemplate = mustParseTemplate("admin-widget.html", "document.html", "footer.html")
adminIndexTemplate = mustParseTemplate("admin.html", "document.html", "footer.html")
adminPageTemplate = mustParseTemplate("admin-page.html", "document.html", "footer.html")
adminWidgetTemplate = mustParseTemplate("admin-widget.html", "document.html", "footer.html")
adminPageSettingsTemplate = mustParseTemplate("admin-page-settings.html", "document.html", "footer.html")
)
// allWidgetTypes mirrors the switch in newWidget(). Aliases ("stocks") omitted.
@@ -208,6 +209,8 @@ type adminPageSummary struct {
Title string
Slug string
WidgetCount int
IsFirst bool
IsLast bool
}
type adminPageDetail struct {
@@ -217,6 +220,16 @@ type adminPageDetail struct {
Columns []adminColumnView
}
type adminPageSettings struct {
Name string
Slug string
Width string
DesktopNavigationWidth string
ShowMobileHeader bool
HideDesktopNavigation bool
CenterVertically bool
}
type adminTemplateData struct {
App *application
Request templateRequestData
@@ -232,6 +245,10 @@ type adminTemplateData struct {
DocsURL string
IsNew bool
ErrorMessage string
PageSettings *adminPageSettings
IsFirstPage bool
IsLastPage bool
}
func (a *application) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
@@ -240,6 +257,7 @@ func (a *application) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
}
pages := a.freshPagesFromDisk()
last := len(pages) - 1
summaries := make([]adminPageSummary, 0, len(pages))
for p := range pages {
page := &pages[p]
@@ -251,6 +269,8 @@ func (a *application) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
Title: page.Title,
Slug: page.Slug,
WidgetCount: count,
IsFirst: p == 0,
IsLast: p == last,
})
}
@@ -363,6 +383,36 @@ func (a *application) handleAdminWidget(w http.ResponseWriter, r *http.Request)
})
}
func (a *application) handleAdminPageSettings(w http.ResponseWriter, r *http.Request) {
if !a.adminAccessAllowed(w, r) {
return
}
pages := a.freshPagesFromDisk()
page, idx, exists := findPageBySlug(pages, r.PathValue("page"))
if !exists {
a.handleNotFound(w, r)
return
}
settings := &adminPageSettings{
Name: page.Title,
Slug: page.Slug,
Width: page.Width,
DesktopNavigationWidth: page.DesktopNavigationWidth,
ShowMobileHeader: page.ShowMobileHeader,
HideDesktopNavigation: page.HideDesktopNavigation,
CenterVertically: page.CenterVertically,
}
data := adminTemplateData{
App: a,
Page: &adminPageDetail{Title: page.Title, Slug: page.Slug},
PageSettings: settings,
IsFirstPage: idx == 0,
IsLastPage: idx == len(pages)-1,
}
a.populateTemplateRequestData(&data.Request, r)
renderAdminTemplate(w, adminPageSettingsTemplate, data)
}
func (a *application) handleAdminNewWidget(w http.ResponseWriter, r *http.Request) {
if !a.adminAccessAllowed(w, r) {
return