aboutsummaryrefslogtreecommitdiffstats
path: root/internal/store/pages.go
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-13 00:38:24 +0300
committergrm <grm@eyesin.space>2026-09-13 00:38:24 +0300
commitd11e4c9544f544b3a024c7ac705a674b4096cf50 (patch)
treea60be98a37b764dfcb655b2738c8a991a248103b /internal/store/pages.go
parente974eb6f3feb06c8951f85c1d34161938905f807 (diff)
downloadblogspace-d11e4c9544f544b3a024c7ac705a674b4096cf50.tar.gz
blogspace-d11e4c9544f544b3a024c7ac705a674b4096cf50.tar.bz2
blogspace-d11e4c9544f544b3a024c7ac705a674b4096cf50.zip
Add the Layout tab: five areas with modules, columns and a menu editor
A blog page is now header, left column, main content, right column and footer, each holding an ordered list of modules (blog title, logo, menu, archive by year/month, recent posts, custom HTML, footer text, RSS link, site map). Side columns have percentage widths, can be hidden, and can keep their space when empty; on phones they stack under the posts. The menu becomes its own ordered list mixing pages and outside links. Announcements are placed per column at its top or bottom. Modules and menu items get tables; the migration derives them from each blog's old theme (nav position, footer text, show-in-nav pages) so existing blogs look the same. Custom HTML is deliberately stored and served as-is (owner's decision; see AGENTS.md for why the blast radius is the blogger's own origin). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'internal/store/pages.go')
-rw-r--r--internal/store/pages.go49
1 files changed, 41 insertions, 8 deletions
diff --git a/internal/store/pages.go b/internal/store/pages.go
index 87d440f..cd0e652 100644
--- a/internal/store/pages.go
+++ b/internal/store/pages.go
@@ -13,13 +13,13 @@ type Page struct {
IntroMD string
IntroHTML string
NavOrder int
- ShowInNav bool
+ ShowInNav bool // derived: the page has a menu entry
IsHome bool
CreatedAt time.Time
PostCount int // filled by ListPages only
}
-const pageCols = `id, blog_id, slug, title, intro_md, intro_html, nav_order, show_in_nav, is_home, created_at`
+const pageCols = `id, blog_id, slug, title, intro_md, intro_html, nav_order, EXISTS (SELECT 1 FROM menu_items m WHERE m.page_id=pages.id), is_home, created_at`
func scanPage(row interface{ Scan(...any) error }) (*Page, error) {
var p Page
@@ -60,16 +60,49 @@ func (s *Store) HomePage(ctx context.Context, blogID int64) (*Page, error) {
return scanPage(s.db.QueryRow(ctx, `SELECT `+pageCols+` FROM pages WHERE blog_id=$1 ORDER BY is_home DESC, nav_order, id LIMIT 1`, blogID))
}
+// CreatePage adds a page at the end of the page list and, when ShowInNav is
+// set, to the end of the menu.
func (s *Store) CreatePage(ctx context.Context, p *Page) (*Page, error) {
- return scanPage(s.db.QueryRow(ctx, `INSERT INTO pages (blog_id, slug, title, intro_md, intro_html, nav_order, show_in_nav)
- VALUES ($1,$2,$3,$4,$5,(SELECT coalesce(max(nav_order),-1)+1 FROM pages WHERE blog_id=$1),$6) RETURNING `+pageCols,
- p.BlogID, p.Slug, p.Title, p.IntroMD, p.IntroHTML, p.ShowInNav))
+ tx, err := s.db.Begin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ defer tx.Rollback(ctx)
+ created, err := scanPage(tx.QueryRow(ctx, `INSERT INTO pages (blog_id, slug, title, intro_md, intro_html, nav_order)
+ VALUES ($1,$2,$3,$4,$5,(SELECT coalesce(max(nav_order),-1)+1 FROM pages WHERE blog_id=$1)) RETURNING `+pageCols,
+ p.BlogID, p.Slug, p.Title, p.IntroMD, p.IntroHTML))
+ if err != nil {
+ return nil, err
+ }
+ if p.ShowInNav {
+ if err := addMenuPage(ctx, tx, p.BlogID, created.ID); err != nil {
+ return nil, err
+ }
+ created.ShowInNav = true
+ }
+ return created, tx.Commit(ctx)
}
+// UpdatePage saves the page and keeps its menu entry in step with ShowInNav.
func (s *Store) UpdatePage(ctx context.Context, p *Page) error {
- _, err := s.db.Exec(ctx, `UPDATE pages SET slug=$3, title=$4, intro_md=$5, intro_html=$6, show_in_nav=$7 WHERE blog_id=$1 AND id=$2`,
- p.BlogID, p.ID, p.Slug, p.Title, p.IntroMD, p.IntroHTML, p.ShowInNav)
- return wrap(err)
+ tx, err := s.db.Begin(ctx)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback(ctx)
+ if _, err := tx.Exec(ctx, `UPDATE pages SET slug=$3, title=$4, intro_md=$5, intro_html=$6 WHERE blog_id=$1 AND id=$2`,
+ p.BlogID, p.ID, p.Slug, p.Title, p.IntroMD, p.IntroHTML); err != nil {
+ return wrap(err)
+ }
+ if p.ShowInNav {
+ err = addMenuPage(ctx, tx, p.BlogID, p.ID)
+ } else {
+ _, err = tx.Exec(ctx, `DELETE FROM menu_items WHERE blog_id=$1 AND page_id=$2`, p.BlogID, p.ID)
+ }
+ if err != nil {
+ return err
+ }
+ return tx.Commit(ctx)
}
func (s *Store) DeletePage(ctx context.Context, blogID, id int64) error {