Started working on pages

This commit is contained in:
Leon Mika 2025-02-16 11:43:22 +11:00
parent e2f159e980
commit ba12398d2f
30 changed files with 1391 additions and 145 deletions

183
services/pages/services.go Normal file
View file

@ -0,0 +1,183 @@
package pages
import (
"context"
"errors"
"lmika.dev/lmika/hugo-cms/models"
"lmika.dev/lmika/hugo-cms/providers/db"
"lmika.dev/lmika/hugo-cms/services/jobs"
"lmika.dev/lmika/hugo-cms/services/sitebuilder"
"lmika.dev/pkg/modash/moslice"
"strings"
"time"
"unicode"
)
type Service struct {
db *db.DB
sb *sitebuilder.Service
jobs *jobs.Service
}
func New(
db *db.DB,
sb *sitebuilder.Service,
jobs *jobs.Service,
) *Service {
return &Service{
db: db,
sb: sb,
jobs: jobs,
}
}
func (s *Service) ListPagesOfSite(ctx context.Context, site models.Site) ([]models.Page, error) {
return s.db.ListPagesOfSite(ctx, site.ID)
}
func (s *Service) GetPage(ctx context.Context, id int) (models.Page, error) {
post, err := s.db.GetPage(ctx, int64(id))
if err != nil {
return models.Page{}, err
}
return post, nil
}
func (s *Service) DeletePage(ctx context.Context, site models.Site, id int) error {
post, err := s.db.GetPage(ctx, int64(id))
if err != nil {
return err
}
if err := s.db.DeletePage(ctx, int64(id)); err != nil {
return err
}
return s.jobs.Queue(ctx, s.sb.DeletePage(site, post))
}
func (s *Service) Create(ctx context.Context, site models.Site, req NewPost) (models.Page, error) {
siteBundles, err := s.db.ListBundles(ctx, site.ID)
if err != nil {
return models.Page{}, err
} else if len(siteBundles) == 0 {
return models.Page{}, errors.New("no bundles found")
}
rootBundle, ok := moslice.FindWhere(siteBundles, func(t models.Bundle) bool {
return t.Name == models.RootBundleName
})
if !ok {
return models.Page{}, errors.New("root bundle not found")
}
publishTime := time.Now()
name := s.normalizePageName(req.Title)
nameProvenance := models.TitleNameProvenance
if name == "" {
// Use the timestamp as the name
name = publishTime.Format("2006-01-02-150405")
nameProvenance = models.DateNameProvenance
}
post := models.Page{
SiteID: site.ID,
BundleID: rootBundle.ID,
Name: s.normalizePageName(req.Title),
NameProvenance: nameProvenance,
Title: req.Title,
Body: req.Body,
State: models.PostStatePublished,
PublishDate: time.Now(),
}
if err := s.save(ctx, site, rootBundle, &post); err != nil {
return models.Page{}, err
}
return post, nil
}
func (s *Service) Update(ctx context.Context, site models.Site, pageID int64, req NewPost) (models.Page, error) {
page, err := s.db.GetPage(ctx, pageID)
if err != nil {
return models.Page{}, err
}
if page.SiteID != site.ID {
return models.Page{}, errors.New("page not found")
}
bundle, err := s.db.GetBundleWithID(ctx, page.BundleID)
if err != nil {
return models.Page{}, err
} else if bundle.SiteID != site.ID {
return models.Page{}, errors.New("page not found")
}
// Update the title if it wasn't set by the user
if page.NameProvenance != models.UserNameProvenance {
if req.Title == "" {
page.Name = page.PublishDate.Format("2006-01-02-150405")
page.NameProvenance = models.DateNameProvenance
} else {
page.Name = s.normalizePageName(req.Title)
page.NameProvenance = models.TitleNameProvenance
}
}
page.Title = req.Title
page.Body = req.Body
if err := s.save(ctx, site, bundle, &page); err != nil {
return models.Page{}, err
}
return page, nil
}
func (s *Service) save(ctx context.Context, site models.Site, bundle models.Bundle, page *models.Page) error {
page.SiteID = site.ID
if page.ID == 0 {
page.CreatedAt = time.Now()
page.UpdatedAt = time.Now()
if err := s.db.InsertPage(ctx, page); err != nil {
return err
}
} else {
page.UpdatedAt = time.Now()
if err := s.db.UpdatePage(ctx, page); err != nil {
return err
}
}
return s.jobs.Queue(ctx, s.sb.WritePage(site, bundle, *page))
}
func (s *Service) normalizePageName(title string) string {
var sb strings.Builder
lastSpace := false
for _, r := range title {
switch {
case unicode.IsSpace(r):
if !lastSpace {
sb.WriteRune('-')
lastSpace = true
}
case unicode.IsNumber(r):
lastSpace = false
sb.WriteRune(r)
case unicode.IsLetter(r):
lastSpace = false
sb.WriteRune(unicode.ToLower(r))
}
}
return sb.String()
}
type NewPost struct {
Title string
Body string
}

View file

@ -55,11 +55,11 @@ func (s *Service) DeletePost(ctx context.Context, site models.Site, id int) erro
func (s *Service) Create(ctx context.Context, site models.Site, req NewPost) (models.Post, error) {
post := models.Post{
SiteID: site.ID,
Title: req.Title,
Body: req.Body,
State: models.PostStatePublished,
PostDate: time.Now(),
SiteID: site.ID,
Title: req.Title,
Body: req.Body,
State: models.PostStatePublished,
PublishDate: time.Now(),
}
if err := s.Save(ctx, site, &post); err != nil {

View file

@ -0,0 +1,105 @@
package sitebuilder
import (
"context"
"fmt"
"lmika.dev/lmika/hugo-cms/models"
"lmika.dev/pkg/modash/momap"
"os"
"time"
)
func (s *Service) WritePage(site models.Site, bundle models.Bundle, page models.Page) models.Job {
return models.Job{
Do: func(ctx context.Context) error {
s.signalSiteBuildingStarted(ctx, site)
defer s.signalSiteBuildingFinished(ctx, site)
rbn, err := s.fullRebuildNecessary(ctx, site)
if err != nil {
return err
} else if rbn {
return s.rebuildSite(ctx, site, site)
}
themeMeta, ok := s.themes.Lookup(site.Theme)
if !ok {
return fmt.Errorf("theme %s not found in themes", site.Theme)
}
if err := s.writePage(site, themeMeta, bundle, page); err != nil {
return err
}
return s.publish(ctx, site)
},
}
}
func (s *Service) DeletePage(site models.Site, page models.Page) models.Job {
return models.Job{
Do: func(ctx context.Context) error {
s.signalSiteBuildingStarted(ctx, site)
defer s.signalSiteBuildingFinished(ctx, site)
bundle, err := s.db.GetBundleWithID(ctx, page.BundleID)
if err != nil {
return err
}
postFilename := s.pageFilename(site, bundle, page)
if os.Remove(postFilename) != nil {
return nil
}
// TODO: if dir is empty, delete it
return s.publish(ctx, site)
},
}
}
func (s *Service) writeAllPages(ctx context.Context, site models.Site) error {
themeMeta, ok := s.themes.Lookup(site.Theme)
if !ok {
return fmt.Errorf("theme %s not found in themes", site.Theme)
}
bundles, err := s.db.ListBundles(ctx, site.ID)
if err != nil {
return err
}
bundlesByID := momap.FromSlice(bundles, func(b models.Bundle) (int64, models.Bundle) { return b.ID, b })
var startId int64
for {
pages, err := s.db.ListPublishablePages(ctx, int64(startId), site.ID)
if err != nil {
return err
} else if len(pages) == 0 {
return nil
}
for _, page := range pages {
if err := s.writePage(site, themeMeta, bundlesByID[page.BundleID], page); err != nil {
return err
}
}
startId = pages[len(pages)-1].ID
}
}
func (s *Service) writePage(site models.Site, themeMeta models.ThemeMeta, bundle models.Bundle, page models.Page) error {
postFilename := s.pageFilename(site, bundle, page)
frontMatter := map[string]any{
"date": page.PublishDate.Format(time.RFC3339),
}
if page.Title != "" {
frontMatter["title"] = page.Title
} else if themeMeta.PreferTitle {
frontMatter["title"] = page.PublishDate.Format(time.ANSIC)
}
return s.writeMarkdownFile(postFilename, frontMatter, page.Body)
}

View file

@ -6,7 +6,6 @@ import (
"gopkg.in/yaml.v3"
"lmika.dev/lmika/hugo-cms/models"
"lmika.dev/lmika/hugo-cms/providers/hugo"
"log"
"os"
"path/filepath"
"time"
@ -104,19 +103,21 @@ func (s *Service) writePost(site models.Site, post models.Post) error {
postFilename := s.postFilename(site, themeMeta, post)
log.Printf(" .. post %v", postFilename)
if err := os.MkdirAll(filepath.Dir(postFilename), 0755); err != nil {
return err
}
frontMatter := map[string]string{
"date": post.PostDate.Format(time.RFC3339),
frontMatter := map[string]any{
"date": post.PublishDate.Format(time.RFC3339),
}
if post.Title != "" {
frontMatter["title"] = post.Title
} else if themeMeta.PreferTitle {
frontMatter["title"] = post.PostDate.Format(time.ANSIC)
frontMatter["title"] = post.PublishDate.Format(time.ANSIC)
}
return s.writeMarkdownFile(postFilename, frontMatter, post.Body)
}
func (s *Service) writeMarkdownFile(outFile string, frontMatter map[string]any, body string) error {
if err := os.MkdirAll(filepath.Dir(outFile), 0755); err != nil {
return err
}
fmBytes, err := yaml.Marshal(frontMatter)
@ -124,7 +125,7 @@ func (s *Service) writePost(site models.Site, post models.Post) error {
return err
}
f, err := os.Create(postFilename)
f, err := os.Create(outFile)
if err != nil {
return err
}
@ -139,7 +140,7 @@ func (s *Service) writePost(site models.Site, post models.Post) error {
if _, err := f.WriteString("---\n"); err != nil {
return err
}
if _, err := f.WriteString(post.Body); err != nil {
if _, err := f.WriteString(body); err != nil {
return err
}
@ -147,5 +148,15 @@ func (s *Service) writePost(site models.Site, post models.Post) error {
}
func (s *Service) postFilename(site models.Site, themeMeta models.ThemeMeta, post models.Post) string {
return filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.PostDir, post.CreatedAt.Format("2006-01-02-150405.md"))
return filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.BlogPostBundle, post.CreatedAt.Format("2006-01-02-150405.md"))
}
func (s *Service) pageFilename(site models.Site, bundle models.Bundle, page models.Page) string {
bundleDir := ""
if bundle.Name != models.RootBundleName {
bundleDir = bundle.Name
}
pageName := page.Name + ".md"
return filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), bundleDir, pageName)
}

View file

@ -79,6 +79,10 @@ func (s *Service) rebuildSite(ctx context.Context, oldSite, newSite models.Site)
return err
}
if err := s.writeAllPages(ctx, newSite); err != nil {
return err
}
return s.publish(ctx, newSite)
}
@ -148,3 +152,4 @@ func (s *Service) signalSiteBuildingStarted(ctx context.Context, site models.Sit
func (s *Service) signalSiteBuildingFinished(ctx context.Context, site models.Site) {
s.bus.Fire(models.Event{Type: models.EventSiteBuildingDone, Data: site})
}

View file

@ -0,0 +1,32 @@
package sitebuilder
import (
"lmika.dev/lmika/hugo-cms/models"
"lmika.dev/lmika/hugo-cms/providers/bus"
)
type SiteBuildingTracker struct {
bus *bus.Bus
isBuildingState map[int64]models.Site
}
func NewSiteBuildingTracker(bus *bus.Bus) *SiteBuildingTracker {
return &SiteBuildingTracker{
bus: bus,
isBuildingState: map[int64]models.Site{},
}
}
func (sbt *SiteBuildingTracker) Listen() {
sub := sbt.bus.Subscribe()
for e := range sub.C {
switch e.Type {
case models.EventSiteBuildingStart:
site := e.Data.(models.Site)
sbt.isBuildingState[site.ID] = site
case models.EventSiteBuildingDone:
delete(sbt.isBuildingState, e.Data.(models.Site).ID)
}
}
}

55
services/sites/create.go Normal file
View file

@ -0,0 +1,55 @@
package sites
import (
"context"
"errors"
"lmika.dev/lmika/hugo-cms/models"
"time"
)
func (s *Service) CreateSite(ctx context.Context, user models.User, name string) (models.Site, error) {
// Create a new site
newSite := models.Site{
Name: normaliseName(name),
OwnerUserID: user.ID,
Title: name,
Theme: "bear",
}
_, ok := s.themes.Lookup(newSite.Theme)
if !ok {
return models.Site{}, errors.New("theme not found")
}
if err := s.db.InsertSite(ctx, &newSite); err != nil {
return models.Site{}, err
}
// Add the default page bundle
rootBundle := models.Bundle{
SiteID: newSite.ID,
Name: models.RootBundleName,
CreatedAt: time.Now(),
}
if err := s.db.InsertBundle(ctx, &rootBundle); err != nil {
return models.Site{}, err
}
// TEMP: Add a home page
homePage := models.Page{
SiteID: newSite.ID,
BundleID: rootBundle.ID,
Name: "index",
Title: "Welcome to the home page",
Body: "This is the home page",
State: models.PostStatePublished,
PublishDate: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.db.InsertPage(ctx, &homePage); err != nil {
return models.Site{}, err
}
return newSite, s.jobs.Queue(ctx, s.sb.RebuildSite(newSite, newSite))
}

View file

@ -46,27 +46,6 @@ func (s *Service) GetProdTargetOfSite(ctx context.Context, siteID int) (models.P
return s.db.GetPublishTargetBySiteRole(ctx, int64(siteID), models.TargetRoleProduction)
}
func (s *Service) CreateSite(ctx context.Context, user models.User, name string) (models.Site, error) {
newSite := models.Site{
Name: normaliseName(name),
OwnerUserID: user.ID,
Title: name,
Theme: "bear",
//Theme: "yingyang",
}
_, ok := s.themes.Lookup(newSite.Theme)
if !ok {
return models.Site{}, errors.New("theme not found")
}
if err := s.db.InsertSite(ctx, &newSite); err != nil {
return models.Site{}, err
}
return newSite, s.jobs.Queue(ctx, s.sb.CreateNewSite(newSite))
}
func (s *Service) SaveSettings(ctx context.Context, site models.Site, newSettings NewSettings) error {
_, ok := s.themes.Lookup(newSettings.SiteTheme)
if !ok {