Have got publishing to Netlify

This commit is contained in:
Leon Mika 2025-01-27 15:45:53 +11:00
parent 8e0ffb6c24
commit 7ef6725bdb
23 changed files with 667 additions and 109 deletions

3
.gitignore vendored
View file

@ -1,2 +1,5 @@
.idea .idea
build/ build/
.env
# Local Netlify folder
.netlify

View file

@ -1,22 +1,30 @@
package config package config
import "path/filepath" import (
"github.com/Netflix/go-env"
"path/filepath"
)
type Config struct { type Config struct {
DatabaseURL string `env:"DATABASE_URL"` DatabaseURL string `env:"DATABASE_URL"`
NetlifyAuthToken string `env:"NETLIFY_AUTH_TOKEN"`
DataDir string `env:"DATA_DIR"` DataDir string `env:"DATA_DIR"`
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"` DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
DataScratchDir string `env:"DATA_SCRATCH_DIR,default=scratch"`
} }
func Load() (Config, error) { func Load() (cfg Config, err error) {
return Config{ _, err = env.UnmarshalFromEnviron(&cfg)
DatabaseURL: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable", if err != nil {
DataDir: "build/data", return Config{}, err
DataStagingDir: "staging", }
}, nil return cfg, nil
} }
func (c Config) StagingDir() string { func (c Config) StagingDir() string {
return filepath.Join(c.DataDir, c.DataStagingDir) return filepath.Join(c.DataDir, c.DataStagingDir)
} }
func (c Config) ScratchDir() string {
return filepath.Join(c.DataDir, c.DataScratchDir)
}

View file

@ -53,6 +53,88 @@ func (ns NullPostState) Value() (driver.Value, error) {
return string(ns.PostState), nil return string(ns.PostState), nil
} }
type TargetRole string
const (
TargetRoleProduction TargetRole = "production"
)
func (e *TargetRole) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TargetRole(s)
case string:
*e = TargetRole(s)
default:
return fmt.Errorf("unsupported scan type for TargetRole: %T", src)
}
return nil
}
type NullTargetRole struct {
TargetRole TargetRole
Valid bool // Valid is true if TargetRole is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullTargetRole) Scan(value interface{}) error {
if value == nil {
ns.TargetRole, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.TargetRole.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullTargetRole) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.TargetRole), nil
}
type TargetType string
const (
TargetTypeNetlify TargetType = "netlify"
)
func (e *TargetType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TargetType(s)
case string:
*e = TargetType(s)
default:
return fmt.Errorf("unsupported scan type for TargetType: %T", src)
}
return nil
}
type NullTargetType struct {
TargetType TargetType
Valid bool // Valid is true if TargetType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullTargetType) Scan(value interface{}) error {
if value == nil {
ns.TargetType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.TargetType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullTargetType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.TargetType), nil
}
type Post struct { type Post struct {
ID int64 ID int64
SiteID int64 SiteID int64
@ -64,6 +146,15 @@ type Post struct {
CreatedAt pgtype.Timestamp CreatedAt pgtype.Timestamp
} }
type PublishTarget struct {
ID int64
SiteID int64
Role TargetRole
TargetType TargetType
Url string
TargetRef string
}
type Site struct { type Site struct {
ID int64 ID int64
Name string Name string

View file

@ -81,3 +81,45 @@ func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
} }
return items, nil return items, nil
} }
const listPublishablePosts = `-- name: ListPublishablePosts :many
SELECT id, site_id, title, body, state, props, post_date, created_at
FROM post
WHERE id > $1 AND site_id = $2 AND state = 'published' AND post_date <= $3
ORDER BY id LIMIT 100
`
type ListPublishablePostsParams struct {
ID int64
SiteID int64
PostDate pgtype.Timestamptz
}
func (q *Queries) ListPublishablePosts(ctx context.Context, arg ListPublishablePostsParams) ([]Post, error) {
rows, err := q.db.Query(ctx, listPublishablePosts, arg.ID, arg.SiteID, arg.PostDate)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Post
for rows.Next() {
var i Post
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Title,
&i.Body,
&i.State,
&i.Props,
&i.PostDate,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View file

@ -0,0 +1,73 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: targets.sql
package dbq
import (
"context"
)
const insertPublishTarget = `-- name: InsertPublishTarget :one
INSERT INTO publish_target (
site_id,
role,
target_type,
url,
target_ref
) VALUES ($1, $2, $3, $4, $5)
RETURNING id
`
type InsertPublishTargetParams struct {
SiteID int64
Role TargetRole
TargetType TargetType
Url string
TargetRef string
}
func (q *Queries) InsertPublishTarget(ctx context.Context, arg InsertPublishTargetParams) (int64, error) {
row := q.db.QueryRow(ctx, insertPublishTarget,
arg.SiteID,
arg.Role,
arg.TargetType,
arg.Url,
arg.TargetRef,
)
var id int64
err := row.Scan(&id)
return id, err
}
const listPublishTargetsOfRole = `-- name: ListPublishTargetsOfRole :many
SELECT id, site_id, role, target_type, url, target_ref FROM publish_target WHERE site_id = $1 AND role = 'production'
`
func (q *Queries) ListPublishTargetsOfRole(ctx context.Context, siteID int64) ([]PublishTarget, error) {
rows, err := q.db.Query(ctx, listPublishTargetsOfRole, siteID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []PublishTarget
for rows.Next() {
var i PublishTarget
if err := rows.Scan(
&i.ID,
&i.SiteID,
&i.Role,
&i.TargetType,
&i.Url,
&i.TargetRef,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

1
go.mod
View file

@ -8,6 +8,7 @@ require (
) )
require ( require (
github.com/Netflix/go-env v0.1.2 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gofiber/fiber/v2 v2.52.6 // indirect github.com/gofiber/fiber/v2 v2.52.6 // indirect

2
go.sum
View file

@ -1,3 +1,5 @@
github.com/Netflix/go-env v0.1.2 h1:0DRoLR9lECQ9Zqvkswuebm3jJ/2enaDX6Ei8/Z+EnK0=
github.com/Netflix/go-env v0.1.2/go.mod h1:WlIhYi++8FlKNJtrop1mjXYAJMzv1f43K4MqCoh0yGE=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -19,7 +19,7 @@ func (s *Site) Create() fiber.Handler {
return err return err
} }
return c.Redirect(fmt.Sprintf("/sites/%v", site.ID)) return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID))
} }
} }
@ -41,6 +41,16 @@ func (s *Site) Show() fiber.Handler {
} }
} }
func (s *Site) Rebuild() fiber.Handler {
return func(c *fiber.Ctx) error {
if err := s.Site.Rebuild(c.UserContext(), GetSite(c)); err != nil {
return err
}
return c.Redirect(fmt.Sprintf("/sites/%v/posts", GetSite(c).ID))
}
}
func (s *Site) WithSite() fiber.Handler { func (s *Site) WithSite() fiber.Handler {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
id, err := c.ParamsInt("siteId") id, err := c.ParamsInt("siteId")

View file

@ -9,6 +9,7 @@ import (
"lmika.dev/lmika/hugo-crm/providers/db" "lmika.dev/lmika/hugo-crm/providers/db"
"lmika.dev/lmika/hugo-crm/providers/git" "lmika.dev/lmika/hugo-crm/providers/git"
"lmika.dev/lmika/hugo-crm/providers/hugo" "lmika.dev/lmika/hugo-crm/providers/hugo"
"lmika.dev/lmika/hugo-crm/providers/netlify"
"lmika.dev/lmika/hugo-crm/providers/themes" "lmika.dev/lmika/hugo-crm/providers/themes"
"lmika.dev/lmika/hugo-crm/services/jobs" "lmika.dev/lmika/hugo-crm/services/jobs"
"lmika.dev/lmika/hugo-crm/services/posts" "lmika.dev/lmika/hugo-crm/services/posts"
@ -31,15 +32,16 @@ func main() {
} }
defer dbp.Close() defer dbp.Close()
hugoProvider, err := hugo.New(cfg.StagingDir()) hugoProvider, err := hugo.New(cfg.StagingDir(), cfg.ScratchDir())
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
gitProvider := git.New() gitProvider := git.New()
themesProvider := themes.New() themesProvider := themes.New()
netlifyProvider := netlify.New(cfg.NetlifyAuthToken)
jobService := jobs.New() jobService := jobs.New()
siteBuilderService := sitebuilder.New(themesProvider, gitProvider, hugoProvider) siteBuilderService := sitebuilder.New(dbp, themesProvider, gitProvider, hugoProvider, netlifyProvider)
siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService) siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService)
postService := posts.New(dbp, siteBuilderService, jobService) postService := posts.New(dbp, siteBuilderService, jobService)
@ -69,6 +71,8 @@ func main() {
siteGroup := app.Group("/sites/:siteId") siteGroup := app.Group("/sites/:siteId")
siteGroup.Use(siteHandlers.WithSite()) siteGroup.Use(siteHandlers.WithSite())
siteGroup.Post("/rebuild", siteHandlers.Rebuild())
siteGroup.Get("/posts", postHandlers.Posts()) siteGroup.Get("/posts", postHandlers.Posts())
siteGroup.Post("/posts", postHandlers.Create()) siteGroup.Post("/posts", postHandlers.Create())

View file

@ -5,3 +5,14 @@ import "context"
type Job struct { type Job struct {
Do func(ctx context.Context) error Do func(ctx context.Context) error
} }
func Jobs(jobs ...Job) Job {
return Job{Do: func(ctx context.Context) error {
for _, job := range jobs {
if err := job.Do(ctx); err != nil {
return err
}
}
return nil
}}
}

22
models/publish.go Normal file
View file

@ -0,0 +1,22 @@
package models
type TargetRole string
const (
TargetRoleProduction TargetRole = "production"
)
type TargetType string
const (
TargetTypeNetlify TargetType = "netlify"
)
type PublishTarget struct {
ID int64
SiteID int64
Role TargetRole
Type TargetType
URL string
TargetRef string
}

View file

@ -6,6 +6,7 @@ import (
"lmika.dev/lmika/hugo-crm/gen/sqlc/dbq" "lmika.dev/lmika/hugo-crm/gen/sqlc/dbq"
"lmika.dev/lmika/hugo-crm/models" "lmika.dev/lmika/hugo-crm/models"
"lmika.dev/pkg/modash/moslice" "lmika.dev/pkg/modash/moslice"
"time"
) )
func (db *DB) ListPostsOfSite(ctx context.Context, siteID int64) ([]models.Post, error) { func (db *DB) ListPostsOfSite(ctx context.Context, siteID int64) ([]models.Post, error) {
@ -14,17 +15,20 @@ func (db *DB) ListPostsOfSite(ctx context.Context, siteID int64) ([]models.Post,
return nil, err return nil, err
} }
return moslice.Map(res, func(p dbq.Post) models.Post { return moslice.Map(res, dbPostToPost), nil
return models.Post{
ID: p.ID,
SiteID: p.SiteID,
Title: p.Title.String,
Body: p.Body,
State: models.PostState(p.State),
PostDate: p.PostDate.Time,
CreatedAt: p.CreatedAt.Time,
} }
}), nil
func (db *DB) ListPublishablePosts(ctx context.Context, fromID, siteID int64, now time.Time) ([]models.Post, error) {
res, err := db.q.ListPublishablePosts(ctx, dbq.ListPublishablePostsParams{
ID: fromID,
SiteID: siteID,
PostDate: pgtype.Timestamptz{Time: now, Valid: true},
})
if err != nil {
return nil, err
}
return moslice.Map(res, dbPostToPost), nil
} }
func (db *DB) InsertPost(ctx context.Context, p *models.Post) error { func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
@ -44,3 +48,15 @@ func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
p.ID = res p.ID = res
return nil return nil
} }
func dbPostToPost(p dbq.Post) models.Post {
return models.Post{
ID: p.ID,
SiteID: p.SiteID,
Title: p.Title.String,
Body: p.Body,
State: models.PostState(p.State),
PostDate: p.PostDate.Time,
CreatedAt: p.CreatedAt.Time,
}
}

42
providers/db/publish.go Normal file
View file

@ -0,0 +1,42 @@
package db
import (
"context"
"lmika.dev/lmika/hugo-crm/gen/sqlc/dbq"
"lmika.dev/lmika/hugo-crm/models"
"lmika.dev/pkg/modash/moslice"
)
func (db *DB) InsertPublishTarget(ctx context.Context, target *models.PublishTarget) error {
id, err := db.q.InsertPublishTarget(ctx, dbq.InsertPublishTargetParams{
SiteID: target.SiteID,
Role: dbq.TargetRole(target.Role),
TargetType: dbq.TargetType(target.Type),
Url: target.URL,
TargetRef: target.TargetRef,
})
if err != nil {
return err
}
target.ID = id
return nil
}
func (db *DB) GetPublishTargets(ctx context.Context, siteID int64) ([]models.PublishTarget, error) {
res, err := db.q.ListPublishTargetsOfRole(ctx, siteID)
if err != nil {
return nil, err
}
return moslice.Map(res, func(m dbq.PublishTarget) models.PublishTarget {
return models.PublishTarget{
ID: m.ID,
SiteID: m.SiteID,
Role: models.TargetRole(m.Role),
Type: models.TargetType(m.TargetType),
URL: m.Url,
TargetRef: m.TargetRef,
}
}), nil
}

View file

@ -14,10 +14,11 @@ import (
type Provider struct { type Provider struct {
stagingDir string stagingDir string
scratchDir string
tmpls *template.Template tmpls *template.Template
} }
func New(stagingDir string) (*Provider, error) { func New(stagingDir, scratchDir string) (*Provider, error) {
ts, err := template.ParseFS(tmpls.FS, "*.tmpl") ts, err := template.ParseFS(tmpls.FS, "*.tmpl")
if err != nil { if err != nil {
return nil, err return nil, err
@ -25,6 +26,7 @@ func New(stagingDir string) (*Provider, error) {
return &Provider{ return &Provider{
stagingDir: stagingDir, stagingDir: stagingDir,
scratchDir: scratchDir,
tmpls: ts, tmpls: ts,
}, nil }, nil
} }
@ -52,6 +54,37 @@ func (p *Provider) NewSite(ctx context.Context, site models.Site) error {
return nil return nil
} }
func (p *Provider) PublishSite(ctx context.Context, site models.Site, target models.PublishTarget) (outDir string, clean func(), err error) {
if err := os.MkdirAll(p.scratchDir, 0755); err != nil {
return "", nil, err
}
outDir, err = os.MkdirTemp(p.scratchDir, site.Name+"-*")
if err != nil {
return "", nil, err
}
clean = func() {
os.RemoveAll(outDir)
}
outDir, err = filepath.Abs(outDir)
if err != nil {
return "", nil, err
}
cmd := exec.CommandContext(ctx, "hugo",
"--source", p.SiteStagingDir(site, BaseSiteDir),
"--destination", outDir,
"--baseURL", target.URL)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return "", clean, err
}
return outDir, clean, nil
}
func (p *Provider) ReconfigureSite(ctx context.Context, site models.Site) error { func (p *Provider) ReconfigureSite(ctx context.Context, site models.Site) error {
// Reconfigure the site // Reconfigure the site
var hugoCfg bytes.Buffer var hugoCfg bytes.Buffer

View file

@ -0,0 +1,29 @@
package netlify
import (
"context"
"fmt"
"lmika.dev/lmika/hugo-crm/models"
"os"
"os/exec"
)
type Provider struct {
authToken string
}
func New(authToken string) *Provider {
return &Provider{
authToken: authToken,
}
}
func (p *Provider) Publish(ctx context.Context, target models.PublishTarget, dir string) error {
cmd := exec.CommandContext(ctx, "netlify", "deploy", "--dir", dir, "--prod")
cmd.Env = append(os.Environ(),
fmt.Sprintf("NETLIFY_SITE_ID=%v", target.TargetRef),
fmt.Sprintf("NETLIFY_AUTH_TOKEN=%v", p.authToken))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

View file

@ -0,0 +1,98 @@
package sitebuilder
import (
"context"
"errors"
"gopkg.in/yaml.v3"
"lmika.dev/lmika/hugo-crm/models"
"lmika.dev/lmika/hugo-crm/providers/hugo"
"log"
"os"
"path/filepath"
"time"
)
func (s *Service) WritePost(site models.Site, post models.Post) models.Job {
return models.Jobs(
models.Job{
Do: func(ctx context.Context) error {
return s.writePost(site, post)
},
},
s.Publish(site),
)
}
func (s *Service) WriteAllPosts(site models.Site) models.Job {
return models.Job{
Do: func(ctx context.Context) error {
var startId int64
now := time.Now()
for {
posts, err := s.db.ListPublishablePosts(ctx, int64(startId), site.ID, now)
if err != nil {
return err
} else if len(posts) == 0 {
return nil
}
for _, post := range posts {
if err := s.writePost(site, post); err != nil {
return err
}
}
startId = posts[len(posts)-1].ID
}
},
}
}
func (s *Service) writePost(site models.Site, post models.Post) error {
themeMeta, ok := s.themes.Lookup(site.Theme)
if !ok {
return errors.New("theme not found")
}
postFilename := filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.PostDir, post.CreatedAt.Format("2006-01-02-150405.md"))
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),
}
if post.Title != "" {
frontMatter["title"] = post.Title
} else if themeMeta.PreferTitle {
frontMatter["title"] = post.PostDate.Format(time.ANSIC)
}
fmBytes, err := yaml.Marshal(frontMatter)
if err != nil {
return err
}
f, err := os.Create(postFilename)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString("---\n"); err != nil {
return err
}
if _, err := f.Write(fmBytes); err != nil {
return err
}
if _, err := f.WriteString("---\n"); err != nil {
return err
}
if _, err := f.WriteString(post.Body); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,40 @@
package sitebuilder
import (
"context"
"lmika.dev/lmika/hugo-crm/models"
)
func (s *Service) Publish(site models.Site) models.Job {
return models.Job{
Do: func(ctx context.Context) error {
targets, err := s.db.GetPublishTargets(ctx, site.ID)
if err != nil {
return err
}
for _, target := range targets {
if err := s.publishTarget(ctx, site, target); err != nil {
return err
}
}
return nil
},
}
}
func (s *Service) publishTarget(ctx context.Context, site models.Site, target models.PublishTarget) error {
outDir, cleanFn, err := s.hugo.PublishSite(ctx, site, target)
//defer func() {
// if cleanFn != nil {
// cleanFn()
// }
//}()
_ = cleanFn
if err != nil {
return err
}
return s.netlify.Publish(ctx, target, outDir)
}

View file

@ -3,38 +3,67 @@ package sitebuilder
import ( import (
"context" "context"
"errors" "errors"
"gopkg.in/yaml.v3"
"lmika.dev/lmika/hugo-crm/models" "lmika.dev/lmika/hugo-crm/models"
"lmika.dev/lmika/hugo-crm/providers/db"
"lmika.dev/lmika/hugo-crm/providers/git" "lmika.dev/lmika/hugo-crm/providers/git"
"lmika.dev/lmika/hugo-crm/providers/hugo" "lmika.dev/lmika/hugo-crm/providers/hugo"
"lmika.dev/lmika/hugo-crm/providers/netlify"
"lmika.dev/lmika/hugo-crm/providers/themes" "lmika.dev/lmika/hugo-crm/providers/themes"
"log" "log"
"os" "os"
"path/filepath"
"time"
) )
type Service struct { type Service struct {
db *db.DB
themes *themes.Provider themes *themes.Provider
git *git.Provider git *git.Provider
hugo *hugo.Provider hugo *hugo.Provider
netlify *netlify.Provider
} }
func New( func New(
db *db.DB,
themes *themes.Provider, themes *themes.Provider,
git *git.Provider, git *git.Provider,
hugo *hugo.Provider, hugo *hugo.Provider,
netlify *netlify.Provider,
) *Service { ) *Service {
return &Service{ return &Service{
db: db,
themes: themes, themes: themes,
git: git, git: git,
hugo: hugo, hugo: hugo,
netlify: netlify,
} }
} }
func (s *Service) CreateNewSite(site models.Site) models.Job { func (s *Service) CreateNewSite(site models.Site) models.Job {
return models.Job{ return models.Job{
Do: func(ctx context.Context) error { Do: func(ctx context.Context) error {
return s.createSite(ctx, site)
},
}
}
func (s *Service) RebuildSite(site models.Site) models.Job {
return models.Jobs(
models.Job{
Do: func(ctx context.Context) error {
// Teardown the existing site
siteDir := s.hugo.SiteStagingDir(site, hugo.BaseSiteDir)
if err := os.RemoveAll(siteDir); err != nil {
return err
}
return nil
},
},
s.CreateNewSite(site),
s.WriteAllPosts(site),
s.Publish(site),
)
}
func (s *Service) createSite(ctx context.Context, site models.Site) error {
themeMeta, ok := s.themes.Lookup(site.Theme) themeMeta, ok := s.themes.Lookup(site.Theme)
if !ok { if !ok {
return errors.New("theme not found") return errors.New("theme not found")
@ -56,60 +85,4 @@ func (s *Service) CreateNewSite(site models.Site) models.Job {
return err return err
} }
return nil return nil
},
}
}
func (s *Service) WritePost(site models.Site, post models.Post) models.Job {
return models.Job{
Do: func(ctx context.Context) error {
themeMeta, ok := s.themes.Lookup(site.Theme)
if !ok {
return errors.New("theme not found")
}
postFilename := filepath.Join(s.hugo.SiteStagingDir(site, hugo.ContentSiteDir), themeMeta.PostDir, post.CreatedAt.Format("2006-01-02-150405.md"))
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),
}
if post.Title != "" {
frontMatter["title"] = post.Title
} else if themeMeta.PreferTitle {
frontMatter["title"] = post.PostDate.Format(time.ANSIC)
}
fmBytes, err := yaml.Marshal(frontMatter)
if err != nil {
return err
}
f, err := os.Create(postFilename)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString("---\n"); err != nil {
return err
}
if _, err := f.Write(fmBytes); err != nil {
return err
}
if _, err := f.WriteString("---\n"); err != nil {
return err
}
if _, err := f.WriteString(post.Body); err != nil {
return err
}
return nil
},
}
} }

View file

@ -57,9 +57,28 @@ func (s *Service) CreateSite(ctx context.Context, name string) (models.Site, err
return models.Site{}, err return models.Site{}, err
} }
// TEMP
if err := s.db.InsertPublishTarget(ctx, &models.PublishTarget{
SiteID: newSite.ID,
Role: models.TargetRoleProduction,
Type: models.TargetTypeNetlify,
URL: "https://meek-meringue-060cfc.netlify.app",
TargetRef: "e628dc6e-e6e1-45a9-847a-982adef940a8",
}); err != nil {
return models.Site{}, err
}
return newSite, s.jobs.Queue(ctx, s.sb.CreateNewSite(newSite)) return newSite, s.jobs.Queue(ctx, s.sb.CreateNewSite(newSite))
} }
func (s *Service) Rebuild(ctx context.Context, site models.Site) error {
if site.ID == 0 {
return errors.New("site id required")
}
return s.jobs.Queue(ctx, s.sb.RebuildSite(site))
}
func normaliseName(name string) string { func normaliseName(name string) string {
var sb strings.Builder var sb strings.Builder

View file

@ -1,6 +1,12 @@
-- name: ListPosts :many -- name: ListPosts :many
SELECT * FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25; SELECT * FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25;
-- name: ListPublishablePosts :many
SELECT *
FROM post
WHERE id > $1 AND site_id = $2 AND state = 'published' AND post_date <= $3
ORDER BY id LIMIT 100;
-- name: InsertPost :one -- name: InsertPost :one
INSERT INTO post ( INSERT INTO post (
site_id, site_id,

12
sql/queries/targets.sql Normal file
View file

@ -0,0 +1,12 @@
-- name: ListPublishTargetsOfRole :many
SELECT * FROM publish_target WHERE site_id = $1 AND role = 'production';
-- name: InsertPublishTarget :one
INSERT INTO publish_target (
site_id,
role,
target_type,
url,
target_ref
) VALUES ($1, $2, $3, $4, $5)
RETURNING id;

View file

@ -3,6 +3,14 @@ CREATE TYPE post_state AS ENUM (
'published' 'published'
); );
CREATE TYPE target_role AS ENUM (
'production'
);
CREATE TYPE target_type AS ENUM (
'netlify'
);
CREATE TABLE site ( CREATE TABLE site (
id BIGSERIAL NOT NULL PRIMARY KEY, id BIGSERIAL NOT NULL PRIMARY KEY,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
@ -24,3 +32,14 @@ CREATE TABLE post (
FOREIGN KEY (site_id) REFERENCES site (id) FOREIGN KEY (site_id) REFERENCES site (id)
); );
CREATE TABLE publish_target (
id BIGSERIAL NOT NULL PRIMARY KEY,
site_id BIGINT NOT NULL,
role target_role NOT NULL,
target_type target_type NOT NULL,
url TEXT NOT NULL,
target_ref TEXT NOT NULL,
FOREIGN KEY (site_id) REFERENCES site (id)
);

View file

@ -5,6 +5,10 @@
<input type="submit" value="Post"> <input type="submit" value="Post">
</form> </form>
<form method="post" action="/sites/{{.site.ID}}/rebuild">
<input type="submit" value="Rebuild">
</form>
{{range .posts}} {{range .posts}}
<div class="post"> <div class="post">
{{.Body}} {{.Body}}