Have got post creation working.
This commit is contained in:
		
							parent
							
								
									63b19a249a
								
							
						
					
					
						commit
						8e0ffb6c24
					
				|  | @ -4,6 +4,66 @@ | |||
| 
 | ||||
| package dbq | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql/driver" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/jackc/pgx/v5/pgtype" | ||||
| ) | ||||
| 
 | ||||
| type PostState string | ||||
| 
 | ||||
| const ( | ||||
| 	PostStateDraft     PostState = "draft" | ||||
| 	PostStatePublished PostState = "published" | ||||
| ) | ||||
| 
 | ||||
| func (e *PostState) Scan(src interface{}) error { | ||||
| 	switch s := src.(type) { | ||||
| 	case []byte: | ||||
| 		*e = PostState(s) | ||||
| 	case string: | ||||
| 		*e = PostState(s) | ||||
| 	default: | ||||
| 		return fmt.Errorf("unsupported scan type for PostState: %T", src) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type NullPostState struct { | ||||
| 	PostState PostState | ||||
| 	Valid     bool // Valid is true if PostState is not NULL
 | ||||
| } | ||||
| 
 | ||||
| // Scan implements the Scanner interface.
 | ||||
| func (ns *NullPostState) Scan(value interface{}) error { | ||||
| 	if value == nil { | ||||
| 		ns.PostState, ns.Valid = "", false | ||||
| 		return nil | ||||
| 	} | ||||
| 	ns.Valid = true | ||||
| 	return ns.PostState.Scan(value) | ||||
| } | ||||
| 
 | ||||
| // Value implements the driver Valuer interface.
 | ||||
| func (ns NullPostState) Value() (driver.Value, error) { | ||||
| 	if !ns.Valid { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	return string(ns.PostState), nil | ||||
| } | ||||
| 
 | ||||
| type Post struct { | ||||
| 	ID        int64 | ||||
| 	SiteID    int64 | ||||
| 	Title     pgtype.Text | ||||
| 	Body      string | ||||
| 	State     PostState | ||||
| 	Props     []byte | ||||
| 	PostDate  pgtype.Timestamptz | ||||
| 	CreatedAt pgtype.Timestamp | ||||
| } | ||||
| 
 | ||||
| type Site struct { | ||||
| 	ID    int64 | ||||
| 	Name  string | ||||
|  |  | |||
							
								
								
									
										83
									
								
								gen/sqlc/dbq/posts.sql.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								gen/sqlc/dbq/posts.sql.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,83 @@ | |||
| // Code generated by sqlc. DO NOT EDIT.
 | ||||
| // versions:
 | ||||
| //   sqlc v1.28.0
 | ||||
| // source: posts.sql
 | ||||
| 
 | ||||
| package dbq | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 
 | ||||
| 	"github.com/jackc/pgx/v5/pgtype" | ||||
| ) | ||||
| 
 | ||||
| const insertPost = `-- name: InsertPost :one | ||||
| INSERT INTO post ( | ||||
|     site_id, | ||||
|     title, | ||||
|     body, | ||||
|     state, | ||||
|     props, | ||||
|     post_date, | ||||
|     created_at | ||||
| ) VALUES ($1, $2, $3, $4, $5, $6, $7) | ||||
| RETURNING id | ||||
| ` | ||||
| 
 | ||||
| type InsertPostParams struct { | ||||
| 	SiteID    int64 | ||||
| 	Title     pgtype.Text | ||||
| 	Body      string | ||||
| 	State     PostState | ||||
| 	Props     []byte | ||||
| 	PostDate  pgtype.Timestamptz | ||||
| 	CreatedAt pgtype.Timestamp | ||||
| } | ||||
| 
 | ||||
| func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64, error) { | ||||
| 	row := q.db.QueryRow(ctx, insertPost, | ||||
| 		arg.SiteID, | ||||
| 		arg.Title, | ||||
| 		arg.Body, | ||||
| 		arg.State, | ||||
| 		arg.Props, | ||||
| 		arg.PostDate, | ||||
| 		arg.CreatedAt, | ||||
| 	) | ||||
| 	var id int64 | ||||
| 	err := row.Scan(&id) | ||||
| 	return id, err | ||||
| } | ||||
| 
 | ||||
| const listPosts = `-- name: ListPosts :many | ||||
| SELECT id, site_id, title, body, state, props, post_date, created_at FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25 | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) { | ||||
| 	rows, err := q.db.Query(ctx, listPosts, siteID) | ||||
| 	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 | ||||
| } | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							|  | @ -41,4 +41,6 @@ require ( | |||
| 	golang.org/x/sync v0.10.0 // indirect | ||||
| 	golang.org/x/sys v0.29.0 // indirect | ||||
| 	golang.org/x/text v0.21.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| 	lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1 // indirect | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							|  | @ -81,3 +81,7 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= | |||
| golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1 h1:Seqp9vlIw3uJBL0V/eWIM3dAnSuToJ/cztkRQtl3g20= | ||||
| lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI= | ||||
|  |  | |||
							
								
								
									
										14
									
								
								handlers/ctx.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								handlers/ctx.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"lmika.dev/lmika/hugo-crm/models" | ||||
| ) | ||||
| 
 | ||||
| type siteKeyType struct{} | ||||
| 
 | ||||
| var siteKey siteKeyType | ||||
| 
 | ||||
| func GetSite(c *fiber.Ctx) models.Site { | ||||
| 	return c.UserContext().Value(siteKey).(models.Site) | ||||
| } | ||||
							
								
								
									
										49
									
								
								handlers/post.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								handlers/post.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"lmika.dev/lmika/hugo-crm/services/posts" | ||||
| ) | ||||
| 
 | ||||
| type Post struct { | ||||
| 	Post *posts.Service | ||||
| } | ||||
| 
 | ||||
| func (h *Post) Posts() fiber.Handler { | ||||
| 	return func(c *fiber.Ctx) error { | ||||
| 		site := GetSite(c) | ||||
| 
 | ||||
| 		posts, err := h.Post.ListPostOfSite(c.UserContext(), site) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		return c.Render("sites/posts", fiber.Map{ | ||||
| 			"site":  site, | ||||
| 			"posts": posts, | ||||
| 		}, "layouts/main") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (h *Post) Create() fiber.Handler { | ||||
| 	type Req struct { | ||||
| 		Body string `json:"body" form:"body"` | ||||
| 	} | ||||
| 
 | ||||
| 	return func(c *fiber.Ctx) error { | ||||
| 		site := GetSite(c) | ||||
| 
 | ||||
| 		var req Req | ||||
| 		if err := c.BodyParser(&req); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		_, err := h.Post.Create(c.UserContext(), site, req.Body) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		return c.Redirect(fmt.Sprintf("/sites/%v/posts", site.ID)) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,6 +1,7 @@ | |||
| package handlers | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"lmika.dev/lmika/hugo-crm/services/sites" | ||||
|  | @ -30,9 +31,29 @@ func (s *Site) Show() fiber.Handler { | |||
| 		} | ||||
| 
 | ||||
| 		site, err := s.Site.GetSite(c.UserContext(), id) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		return c.Render("sites/index", fiber.Map{ | ||||
| 			"site": site, | ||||
| 		}, "layouts/main") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Site) WithSite() fiber.Handler { | ||||
| 	return func(c *fiber.Ctx) error { | ||||
| 		id, err := c.ParamsInt("siteId") | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		site, err := s.Site.GetSite(c.UserContext(), id) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		c.SetUserContext(context.WithValue(c.UserContext(), siteKey, site)) | ||||
| 		return c.Next() | ||||
| 	} | ||||
| } | ||||
|  |  | |||
							
								
								
									
										9
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								main.go
									
									
									
									
									
								
							|  | @ -11,6 +11,7 @@ import ( | |||
| 	"lmika.dev/lmika/hugo-crm/providers/hugo" | ||||
| 	"lmika.dev/lmika/hugo-crm/providers/themes" | ||||
| 	"lmika.dev/lmika/hugo-crm/services/jobs" | ||||
| 	"lmika.dev/lmika/hugo-crm/services/posts" | ||||
| 	"lmika.dev/lmika/hugo-crm/services/sitebuilder" | ||||
| 	"lmika.dev/lmika/hugo-crm/services/sites" | ||||
| 	"lmika.dev/lmika/hugo-crm/templates" | ||||
|  | @ -41,8 +42,10 @@ func main() { | |||
| 	siteBuilderService := sitebuilder.New(themesProvider, gitProvider, hugoProvider) | ||||
| 
 | ||||
| 	siteService := sites.NewService(cfg, dbp, themesProvider, siteBuilderService, jobService) | ||||
| 	postService := posts.New(dbp, siteBuilderService, jobService) | ||||
| 
 | ||||
| 	siteHandlers := handlers.Site{Site: siteService} | ||||
| 	postHandlers := handlers.Post{Post: postService} | ||||
| 
 | ||||
| 	log.Println("Connected to database") | ||||
| 	if err := dbp.Migrate(context.Background()); err != nil { | ||||
|  | @ -63,6 +66,12 @@ func main() { | |||
| 	app.Post("/sites", siteHandlers.Create()) | ||||
| 	app.Get("/sites/:siteId", siteHandlers.Show()) | ||||
| 
 | ||||
| 	siteGroup := app.Group("/sites/:siteId") | ||||
| 	siteGroup.Use(siteHandlers.WithSite()) | ||||
| 
 | ||||
| 	siteGroup.Get("/posts", postHandlers.Posts()) | ||||
| 	siteGroup.Post("/posts", postHandlers.Create()) | ||||
| 
 | ||||
| 	jobService.Start() | ||||
| 	defer jobService.Stop() | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										20
									
								
								models/posts.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								models/posts.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| package models | ||||
| 
 | ||||
| import "time" | ||||
| 
 | ||||
| type PostState string | ||||
| 
 | ||||
| const ( | ||||
| 	PostStateDraft     PostState = "draft" | ||||
| 	PostStatePublished PostState = "published" | ||||
| ) | ||||
| 
 | ||||
| type Post struct { | ||||
| 	ID        int64 | ||||
| 	SiteID    int64 | ||||
| 	Title     string | ||||
| 	Body      string | ||||
| 	State     PostState | ||||
| 	PostDate  time.Time | ||||
| 	CreatedAt time.Time | ||||
| } | ||||
|  | @ -3,4 +3,10 @@ package models | |||
| type ThemeMeta struct { | ||||
| 	Name string `json:"name"` | ||||
| 	URL  string `json:"repo"` | ||||
| 
 | ||||
| 	// Indicates that this theme prefers posts have titles.
 | ||||
| 	PreferTitle bool | ||||
| 
 | ||||
| 	// Content directory for "blog" posts
 | ||||
| 	PostDir string `json:"post_dir"` | ||||
| } | ||||
|  |  | |||
							
								
								
									
										46
									
								
								providers/db/posts.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								providers/db/posts.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| package db | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/jackc/pgx/v5/pgtype" | ||||
| 	"lmika.dev/lmika/hugo-crm/gen/sqlc/dbq" | ||||
| 	"lmika.dev/lmika/hugo-crm/models" | ||||
| 	"lmika.dev/pkg/modash/moslice" | ||||
| ) | ||||
| 
 | ||||
| func (db *DB) ListPostsOfSite(ctx context.Context, siteID int64) ([]models.Post, error) { | ||||
| 	res, err := db.q.ListPosts(ctx, siteID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return moslice.Map(res, func(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, | ||||
| 		} | ||||
| 	}), nil | ||||
| } | ||||
| 
 | ||||
| func (db *DB) InsertPost(ctx context.Context, p *models.Post) error { | ||||
| 	res, err := db.q.InsertPost(ctx, dbq.InsertPostParams{ | ||||
| 		SiteID:    p.SiteID, | ||||
| 		Title:     pgtype.Text{String: p.Title, Valid: p.Title != ""}, | ||||
| 		Body:      p.Body, | ||||
| 		State:     dbq.PostState(p.State), | ||||
| 		Props:     []byte(`{}`), | ||||
| 		PostDate:  pgtype.Timestamptz{Time: p.PostDate, Valid: !p.PostDate.IsZero()}, | ||||
| 		CreatedAt: pgtype.Timestamp{Time: p.CreatedAt, Valid: !p.CreatedAt.IsZero()}, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	p.ID = res | ||||
| 	return nil | ||||
| } | ||||
|  | @ -3,6 +3,7 @@ package hugo | |||
| type SiteDir string | ||||
| 
 | ||||
| const ( | ||||
| 	BaseSiteDir  SiteDir = "base" | ||||
| 	ThemeSiteDir SiteDir = "theme" | ||||
| 	BaseSiteDir    SiteDir = "base" | ||||
| 	ThemeSiteDir   SiteDir = "theme" | ||||
| 	ContentSiteDir SiteDir = "content" | ||||
| ) | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ func (p *Provider) SiteStagingDir(site models.Site, what SiteDir) string { | |||
| 	case ThemeSiteDir: | ||||
| 		return filepath.Join(p.stagingDir, site.Name, "themes", site.Theme) | ||||
| 	} | ||||
| 	return "" | ||||
| 	return filepath.Join(p.stagingDir, site.Name, string(what)) | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) NewSite(ctx context.Context, site models.Site) error { | ||||
|  |  | |||
|  | @ -4,7 +4,9 @@ import "lmika.dev/lmika/hugo-crm/models" | |||
| 
 | ||||
| var themes = map[string]models.ThemeMeta{ | ||||
| 	"bear": models.ThemeMeta{ | ||||
| 		Name: "bear", | ||||
| 		URL:  "https://github.com/janraasch/hugo-bearblog", | ||||
| 		Name:        "bear", | ||||
| 		URL:         "https://github.com/janraasch/hugo-bearblog", | ||||
| 		PreferTitle: true, | ||||
| 		PostDir:     "blog", | ||||
| 	}, | ||||
| } | ||||
|  |  | |||
							
								
								
									
										47
									
								
								services/posts/services.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								services/posts/services.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| package posts | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"lmika.dev/lmika/hugo-crm/models" | ||||
| 	"lmika.dev/lmika/hugo-crm/providers/db" | ||||
| 	"lmika.dev/lmika/hugo-crm/services/jobs" | ||||
| 	"lmika.dev/lmika/hugo-crm/services/sitebuilder" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| 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) ListPostOfSite(ctx context.Context, site models.Site) ([]models.Post, error) { | ||||
| 	return s.db.ListPostsOfSite(ctx, site.ID) | ||||
| } | ||||
| 
 | ||||
| func (s *Service) Create(ctx context.Context, site models.Site, body string) (models.Post, error) { | ||||
| 	post := models.Post{ | ||||
| 		SiteID:    site.ID, | ||||
| 		Body:      body, | ||||
| 		State:     models.PostStatePublished, | ||||
| 		PostDate:  time.Now(), | ||||
| 		CreatedAt: time.Now(), | ||||
| 	} | ||||
| 	if err := s.db.InsertPost(ctx, &post); err != nil { | ||||
| 		return models.Post{}, err | ||||
| 	} | ||||
| 
 | ||||
| 	return post, s.jobs.Queue(ctx, s.sb.WritePost(site, post)) | ||||
| } | ||||
|  | @ -3,11 +3,15 @@ package sitebuilder | |||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"gopkg.in/yaml.v3" | ||||
| 	"lmika.dev/lmika/hugo-crm/models" | ||||
| 	"lmika.dev/lmika/hugo-crm/providers/git" | ||||
| 	"lmika.dev/lmika/hugo-crm/providers/hugo" | ||||
| 	"lmika.dev/lmika/hugo-crm/providers/themes" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Service struct { | ||||
|  | @ -55,3 +59,57 @@ func (s *Service) CreateNewSite(site models.Site) models.Job { | |||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 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 | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  |  | |||
							
								
								
									
										14
									
								
								sql/queries/posts.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								sql/queries/posts.sql
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| -- name: ListPosts :many | ||||
| SELECT * FROM post WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25; | ||||
| 
 | ||||
| -- name: InsertPost :one | ||||
| INSERT INTO post ( | ||||
|     site_id, | ||||
|     title, | ||||
|     body, | ||||
|     state, | ||||
|     props, | ||||
|     post_date, | ||||
|     created_at | ||||
| ) VALUES ($1, $2, $3, $4, $5, $6, $7) | ||||
| RETURNING id; | ||||
|  | @ -1,8 +1,26 @@ | |||
| CREATE TYPE post_state AS ENUM ( | ||||
|     'draft', | ||||
|     'published' | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE site ( | ||||
|     id    BIGSERIAL NOT NULL PRIMARY KEY, | ||||
|     name  TEXT NOT NULL UNIQUE, | ||||
|     title TEXT NOT NULL, | ||||
|     url   TEXT NOT NULL, | ||||
|     theme TEXT NOT NULL, | ||||
|     props JSON NOT NULL | ||||
|     name  TEXT      NOT NULL UNIQUE, | ||||
|     title TEXT      NOT NULL, | ||||
|     url   TEXT      NOT NULL, | ||||
|     theme TEXT      NOT NULL, | ||||
|     props JSON      NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE post ( | ||||
|     id         BIGSERIAL  NOT NULL PRIMARY KEY, | ||||
|     site_id    BIGINT     NOT NULL, | ||||
|     title      TEXT, | ||||
|     body       TEXT       NOT NULL, | ||||
|     state      post_state NOT NULL, | ||||
|     props      JSON       NOT NULL, | ||||
|     post_date  TIMESTAMP WITH TIME ZONE, | ||||
|     created_at TIMESTAMP  NOT NULL, | ||||
| 
 | ||||
|     FOREIGN KEY (site_id) REFERENCES site(id) | ||||
| ); | ||||
|  | @ -1 +0,0 @@ | |||
| <h1>Site {{.site.Title}}</h1> | ||||
							
								
								
									
										15
									
								
								templates/sites/posts.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								templates/sites/posts.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| <h1>Site {{.site.Title}}</h1> | ||||
| 
 | ||||
| <form method="post" action="/sites/{{.site.ID}}/posts"> | ||||
|     <textarea name="body"></textarea> | ||||
|     <input type="submit" value="Post"> | ||||
| </form> | ||||
| 
 | ||||
| {{range .posts}} | ||||
|     <div class="post"> | ||||
|         {{.Body}} | ||||
|     </div> | ||||
|     <hr> | ||||
| {{else}} | ||||
|     <p>No posts yet</p> | ||||
| {{end}} | ||||
		Loading…
	
		Reference in a new issue