Started working on pages
This commit is contained in:
parent
e2f159e980
commit
ba12398d2f
30 changed files with 1391 additions and 145 deletions
183
services/pages/services.go
Normal file
183
services/pages/services.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
105
services/sitebuilder/pages.go
Normal file
105
services/sitebuilder/pages.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
}
|
||||
|
||||
|
|
|
|||
32
services/sitebuilder/tracker.go
Normal file
32
services/sitebuilder/tracker.go
Normal 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
55
services/sites/create.go
Normal 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))
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue