package store import ( "context" "time" ) type Page struct { ID int64 Slug string Title string IntroMD string IntroHTML string NavOrder int ShowInNav bool // derived: the page has a menu entry IsHome bool CreatedAt time.Time PostCount int // filled by ListPages only } const pageCols = `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 err := row.Scan(&p.ID, &p.Slug, &p.Title, &p.IntroMD, &p.IntroHTML, &p.NavOrder, &p.ShowInNav, &p.IsHome, &p.CreatedAt) if err != nil { return nil, wrap(err) } return &p, nil } func (bs *BlogStore) ListPages(ctx context.Context) ([]Page, error) { rows, err := bs.db.Query(ctx, `SELECT `+pageCols+`, (SELECT count(*) FROM posts WHERE page_id=pages.id) FROM pages ORDER BY nav_order, id`) if err != nil { return nil, err } defer rows.Close() var out []Page for rows.Next() { var p Page if err := rows.Scan(&p.ID, &p.Slug, &p.Title, &p.IntroMD, &p.IntroHTML, &p.NavOrder, &p.ShowInNav, &p.IsHome, &p.CreatedAt, &p.PostCount); err != nil { return nil, err } out = append(out, p) } return out, rows.Err() } func (bs *BlogStore) PageByID(ctx context.Context, id int64) (*Page, error) { return scanPage(bs.db.QueryRow(ctx, `SELECT `+pageCols+` FROM pages WHERE id=$1`, id)) } func (bs *BlogStore) PageBySlug(ctx context.Context, slug string) (*Page, error) { return scanPage(bs.db.QueryRow(ctx, `SELECT `+pageCols+` FROM pages WHERE slug=$1`, slug)) } func (bs *BlogStore) HomePage(ctx context.Context) (*Page, error) { return scanPage(bs.db.QueryRow(ctx, `SELECT `+pageCols+` FROM pages ORDER BY is_home DESC, nav_order, id LIMIT 1`)) } // CreatePage adds a page at the end of the page list and, when ShowInNav is // set, to the end of the menu. func (bs *BlogStore) CreatePage(ctx context.Context, p *Page) (*Page, error) { tx, err := bs.db.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) created, err := scanPage(tx.QueryRow(ctx, `INSERT INTO pages (slug, title, intro_md, intro_html, nav_order) VALUES ($1,$2,$3,$4,(SELECT coalesce(max(nav_order),-1)+1 FROM pages)) RETURNING `+pageCols, p.Slug, p.Title, p.IntroMD, p.IntroHTML)) if err != nil { return nil, err } if p.ShowInNav { if err := addMenuPage(ctx, tx, 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 (bs *BlogStore) UpdatePage(ctx context.Context, p *Page) error { tx, err := bs.db.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) if _, err := tx.Exec(ctx, `UPDATE pages SET slug=$2, title=$3, intro_md=$4, intro_html=$5 WHERE id=$1`, p.ID, p.Slug, p.Title, p.IntroMD, p.IntroHTML); err != nil { return wrap(err) } if p.ShowInNav { err = addMenuPage(ctx, tx, p.ID) } else { _, err = tx.Exec(ctx, `DELETE FROM menu_items WHERE page_id=$1`, p.ID) } if err != nil { return err } return tx.Commit(ctx) } func (bs *BlogStore) DeletePage(ctx context.Context, id int64) error { _, err := bs.db.Exec(ctx, `DELETE FROM pages WHERE id=$1 AND NOT is_home`, id) return err } // SetHomePage moves the home flag to the given page. func (bs *BlogStore) SetHomePage(ctx context.Context, id int64) error { tx, err := bs.db.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) if _, err := tx.Exec(ctx, `UPDATE pages SET is_home=false WHERE is_home`); err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE pages SET is_home=true WHERE id=$1`, id); err != nil { return err } return tx.Commit(ctx) } // MovePage swaps nav_order with the neighbouring page (dir = -1 up, +1 down). func (bs *BlogStore) MovePage(ctx context.Context, id int64, dir int) error { pages, err := bs.ListPages(ctx) if err != nil { return err } idx := -1 for i := range pages { if pages[i].ID == id { idx = i } } j := idx + dir if idx < 0 || j < 0 || j >= len(pages) { return nil } pages[idx], pages[j] = pages[j], pages[idx] tx, err := bs.db.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) for i, p := range pages { // renumber everything; keeps orders dense if _, err := tx.Exec(ctx, `UPDATE pages SET nav_order=$2 WHERE id=$1`, p.ID, i); err != nil { return err } } return tx.Commit(ctx) }