Compare commits
6 commits
3a0ab175ed
...
c5925e16e0
Author | SHA1 | Date | |
---|---|---|---|
|
c5925e16e0 | ||
|
295811411e | ||
|
68aa9c0e13 | ||
|
3cf4294e87 | ||
|
573517565d | ||
|
ba12398d2f |
|
@ -10,9 +10,11 @@ type Config struct {
|
||||||
NetlifyAuthToken string `env:"NETLIFY_AUTH_TOKEN"`
|
NetlifyAuthToken string `env:"NETLIFY_AUTH_TOKEN"`
|
||||||
DataDir string `env:"DATA_DIR"`
|
DataDir string `env:"DATA_DIR"`
|
||||||
EncryptedCookieKey string `env:"ENCRYPTED_COOKIE_KEY"`
|
EncryptedCookieKey string `env:"ENCRYPTED_COOKIE_KEY"`
|
||||||
|
BaseURL string `env:"BASE_URL,default=http://localhost:3000/"`
|
||||||
|
|
||||||
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
|
DataStagingDir string `env:"DATA_STAGING_DIR,default=staging"`
|
||||||
DataScratchDir string `env:"DATA_SCRATCH_DIR,default=scratch"`
|
DataScratchDir string `env:"DATA_SCRATCH_DIR,default=scratch"`
|
||||||
|
DataPreviewDir string `env:"DATA_PREVIEW_DIR,default=preview"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() (cfg Config, err error) {
|
func Load() (cfg Config, err error) {
|
||||||
|
@ -27,6 +29,10 @@ func (c Config) StagingDir() string {
|
||||||
return filepath.Join(c.DataDir, c.DataStagingDir)
|
return filepath.Join(c.DataDir, c.DataStagingDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Config) PreviewDir() string {
|
||||||
|
return filepath.Join(c.DataDir, c.DataPreviewDir)
|
||||||
|
}
|
||||||
|
|
||||||
func (c Config) ScratchDir() string {
|
func (c Config) ScratchDir() string {
|
||||||
return filepath.Join(c.DataDir, c.DataScratchDir)
|
return filepath.Join(c.DataDir, c.DataScratchDir)
|
||||||
}
|
}
|
||||||
|
|
116
gen/sqlc/dbq/bundles.sql.go
Normal file
116
gen/sqlc/dbq/bundles.sql.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: bundles.sql
|
||||||
|
|
||||||
|
package dbq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getBundleWithID = `-- name: GetBundleWithID :one
|
||||||
|
SELECT id, site_id, name, created_at, updated_at FROM bundles WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetBundleWithID(ctx context.Context, id int64) (Bundle, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getBundleWithID, id)
|
||||||
|
var i Bundle
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.Name,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSiteBundleInfo = `-- name: GetSiteBundleInfo :many
|
||||||
|
WITH page_counts AS (
|
||||||
|
SELECT b.bundle_id, count(*) AS page_count FROM pages b WHERE b.site_id = $1 GROUP BY bundle_id
|
||||||
|
), index_pages AS (
|
||||||
|
SELECT p.id AS index_page_id, p.bundle_id FROM pages p WHERE p.site_id = $1 AND p.role = 'index'
|
||||||
|
)
|
||||||
|
SELECT b.bundle_id, b.page_count, p.index_page_id FROM page_counts b LEFT OUTER JOIN index_pages p ON b.bundle_id = p.bundle_id
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetSiteBundleInfoRow struct {
|
||||||
|
BundleID int64
|
||||||
|
PageCount int64
|
||||||
|
IndexPageID pgtype.Int8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetSiteBundleInfo(ctx context.Context, siteID int64) ([]GetSiteBundleInfoRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getSiteBundleInfo, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetSiteBundleInfoRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetSiteBundleInfoRow
|
||||||
|
if err := rows.Scan(&i.BundleID, &i.PageCount, &i.IndexPageID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertBundle = `-- name: InsertBundle :one
|
||||||
|
INSERT INTO bundles (
|
||||||
|
site_id,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES ($1, $2, $3, $3) RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertBundleParams struct {
|
||||||
|
SiteID int64
|
||||||
|
Name string
|
||||||
|
CreatedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertBundle(ctx context.Context, arg InsertBundleParams) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, insertBundle, arg.SiteID, arg.Name, arg.CreatedAt)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listBundles = `-- name: ListBundles :many
|
||||||
|
SELECT id, site_id, name, created_at, updated_at FROM bundles WHERE site_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListBundles(ctx context.Context, siteID int64) ([]Bundle, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listBundles, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Bundle
|
||||||
|
for rows.Next() {
|
||||||
|
var i Bundle
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.Name,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
|
@ -11,6 +11,90 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PageNameProvenance string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PageNameProvenanceUser PageNameProvenance = "user"
|
||||||
|
PageNameProvenanceTitle PageNameProvenance = "title"
|
||||||
|
PageNameProvenanceDate PageNameProvenance = "date"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *PageNameProvenance) Scan(src interface{}) error {
|
||||||
|
switch s := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
*e = PageNameProvenance(s)
|
||||||
|
case string:
|
||||||
|
*e = PageNameProvenance(s)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported scan type for PageNameProvenance: %T", src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullPageNameProvenance struct {
|
||||||
|
PageNameProvenance PageNameProvenance
|
||||||
|
Valid bool // Valid is true if PageNameProvenance is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (ns *NullPageNameProvenance) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
ns.PageNameProvenance, ns.Valid = "", false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ns.Valid = true
|
||||||
|
return ns.PageNameProvenance.Scan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (ns NullPageNameProvenance) Value() (driver.Value, error) {
|
||||||
|
if !ns.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return string(ns.PageNameProvenance), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageRole string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PageRoleIndex PageRole = "index"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *PageRole) Scan(src interface{}) error {
|
||||||
|
switch s := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
*e = PageRole(s)
|
||||||
|
case string:
|
||||||
|
*e = PageRole(s)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported scan type for PageRole: %T", src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullPageRole struct {
|
||||||
|
PageRole PageRole
|
||||||
|
Valid bool // Valid is true if PageRole is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (ns *NullPageRole) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
ns.PageRole, ns.Valid = "", false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ns.Valid = true
|
||||||
|
return ns.PageRole.Scan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (ns NullPageRole) Value() (driver.Value, error) {
|
||||||
|
if !ns.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return string(ns.PageRole), nil
|
||||||
|
}
|
||||||
|
|
||||||
type PostState string
|
type PostState string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -135,15 +219,48 @@ func (ns NullTargetType) Value() (driver.Value, error) {
|
||||||
return string(ns.TargetType), nil
|
return string(ns.TargetType), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Post struct {
|
type Bundle struct {
|
||||||
ID int64
|
ID int64
|
||||||
SiteID int64
|
SiteID int64
|
||||||
Title pgtype.Text
|
Name string
|
||||||
Body string
|
|
||||||
State PostState
|
|
||||||
Props []byte
|
|
||||||
PostDate pgtype.Timestamptz
|
|
||||||
CreatedAt pgtype.Timestamp
|
CreatedAt pgtype.Timestamp
|
||||||
|
UpdatedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
BundleID int64
|
||||||
|
Name string
|
||||||
|
NameProvenance PageNameProvenance
|
||||||
|
Title pgtype.Text
|
||||||
|
PostTypeID pgtype.Int8
|
||||||
|
Body string
|
||||||
|
State PostState
|
||||||
|
Props []byte
|
||||||
|
Role NullPageRole
|
||||||
|
PublishDate pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamp
|
||||||
|
UpdatedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
Title pgtype.Text
|
||||||
|
PostTypeID pgtype.Int8
|
||||||
|
Body string
|
||||||
|
State PostState
|
||||||
|
Props []byte
|
||||||
|
PublishDate pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamp
|
||||||
|
UpdatedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostType struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
LayoutName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublishTarget struct {
|
type PublishTarget struct {
|
||||||
|
|
242
gen/sqlc/dbq/pages.sql.go
Normal file
242
gen/sqlc/dbq/pages.sql.go
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: pages.sql
|
||||||
|
|
||||||
|
package dbq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const deletePageWithID = `-- name: DeletePageWithID :exec
|
||||||
|
DELETE FROM pages WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeletePageWithID(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, deletePageWithID, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPageWithID = `-- name: GetPageWithID :one
|
||||||
|
SELECT id, site_id, bundle_id, name, name_provenance, title, post_type_id, body, state, props, role, publish_date, created_at, updated_at FROM pages WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPageWithID(ctx context.Context, id int64) (Page, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getPageWithID, id)
|
||||||
|
var i Page
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.BundleID,
|
||||||
|
&i.Name,
|
||||||
|
&i.NameProvenance,
|
||||||
|
&i.Title,
|
||||||
|
&i.PostTypeID,
|
||||||
|
&i.Body,
|
||||||
|
&i.State,
|
||||||
|
&i.Props,
|
||||||
|
&i.Role,
|
||||||
|
&i.PublishDate,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertPage = `-- name: InsertPage :one
|
||||||
|
INSERT INTO pages (
|
||||||
|
site_id,
|
||||||
|
bundle_id,
|
||||||
|
name,
|
||||||
|
name_provenance,
|
||||||
|
title,
|
||||||
|
post_type_id,
|
||||||
|
body,
|
||||||
|
state,
|
||||||
|
props,
|
||||||
|
role,
|
||||||
|
publish_date,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $12)
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertPageParams struct {
|
||||||
|
SiteID int64
|
||||||
|
BundleID int64
|
||||||
|
Name string
|
||||||
|
NameProvenance PageNameProvenance
|
||||||
|
Title pgtype.Text
|
||||||
|
PostTypeID pgtype.Int8
|
||||||
|
Body string
|
||||||
|
State PostState
|
||||||
|
Props []byte
|
||||||
|
Role NullPageRole
|
||||||
|
PublishDate pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertPage(ctx context.Context, arg InsertPageParams) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, insertPage,
|
||||||
|
arg.SiteID,
|
||||||
|
arg.BundleID,
|
||||||
|
arg.Name,
|
||||||
|
arg.NameProvenance,
|
||||||
|
arg.Title,
|
||||||
|
arg.PostTypeID,
|
||||||
|
arg.Body,
|
||||||
|
arg.State,
|
||||||
|
arg.Props,
|
||||||
|
arg.Role,
|
||||||
|
arg.PublishDate,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listPages = `-- name: ListPages :many
|
||||||
|
SELECT id, site_id, bundle_id, name, name_provenance, title, post_type_id, body, state, props, role, publish_date, created_at, updated_at FROM pages WHERE site_id = $1 ORDER BY name ASC LIMIT 25
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListPages(ctx context.Context, siteID int64) ([]Page, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listPages, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Page
|
||||||
|
for rows.Next() {
|
||||||
|
var i Page
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.BundleID,
|
||||||
|
&i.Name,
|
||||||
|
&i.NameProvenance,
|
||||||
|
&i.Title,
|
||||||
|
&i.PostTypeID,
|
||||||
|
&i.Body,
|
||||||
|
&i.State,
|
||||||
|
&i.Props,
|
||||||
|
&i.Role,
|
||||||
|
&i.PublishDate,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listPublishablePages = `-- name: ListPublishablePages :many
|
||||||
|
SELECT id, site_id, bundle_id, name, name_provenance, title, post_type_id, body, state, props, role, publish_date, created_at, updated_at
|
||||||
|
FROM pages
|
||||||
|
WHERE id > $1 AND site_id = $2 AND state = 'published'
|
||||||
|
ORDER BY id LIMIT 100
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListPublishablePagesParams struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListPublishablePages(ctx context.Context, arg ListPublishablePagesParams) ([]Page, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listPublishablePages, arg.ID, arg.SiteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Page
|
||||||
|
for rows.Next() {
|
||||||
|
var i Page
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.SiteID,
|
||||||
|
&i.BundleID,
|
||||||
|
&i.Name,
|
||||||
|
&i.NameProvenance,
|
||||||
|
&i.Title,
|
||||||
|
&i.PostTypeID,
|
||||||
|
&i.Body,
|
||||||
|
&i.State,
|
||||||
|
&i.Props,
|
||||||
|
&i.Role,
|
||||||
|
&i.PublishDate,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePage = `-- name: UpdatePage :exec
|
||||||
|
UPDATE pages SET
|
||||||
|
site_id = $2,
|
||||||
|
bundle_id = $3,
|
||||||
|
name = $4,
|
||||||
|
name_provenance = $5,
|
||||||
|
title = $6,
|
||||||
|
post_type_id = $7,
|
||||||
|
role = $8,
|
||||||
|
body = $9,
|
||||||
|
state = $10,
|
||||||
|
props = $11,
|
||||||
|
publish_date = $12,
|
||||||
|
created_at = $13,
|
||||||
|
updated_at = $14
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdatePageParams struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
BundleID int64
|
||||||
|
Name string
|
||||||
|
NameProvenance PageNameProvenance
|
||||||
|
Title pgtype.Text
|
||||||
|
PostTypeID pgtype.Int8
|
||||||
|
Role NullPageRole
|
||||||
|
Body string
|
||||||
|
State PostState
|
||||||
|
Props []byte
|
||||||
|
PublishDate pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamp
|
||||||
|
UpdatedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdatePage(ctx context.Context, arg UpdatePageParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updatePage,
|
||||||
|
arg.ID,
|
||||||
|
arg.SiteID,
|
||||||
|
arg.BundleID,
|
||||||
|
arg.Name,
|
||||||
|
arg.NameProvenance,
|
||||||
|
arg.Title,
|
||||||
|
arg.PostTypeID,
|
||||||
|
arg.Role,
|
||||||
|
arg.Body,
|
||||||
|
arg.State,
|
||||||
|
arg.Props,
|
||||||
|
arg.PublishDate,
|
||||||
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ func (q *Queries) DeletePost(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPostWithID = `-- name: GetPostWithID :one
|
const getPostWithID = `-- name: GetPostWithID :one
|
||||||
SELECT id, site_id, title, body, state, props, post_date, created_at FROM posts WHERE id = $1 LIMIT 1
|
SELECT id, site_id, title, post_type_id, body, state, props, publish_date, created_at, updated_at FROM posts WHERE id = $1 LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetPostWithID(ctx context.Context, id int64) (Post, error) {
|
func (q *Queries) GetPostWithID(ctx context.Context, id int64) (Post, error) {
|
||||||
|
@ -31,11 +31,13 @@ func (q *Queries) GetPostWithID(ctx context.Context, id int64) (Post, error) {
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.SiteID,
|
&i.SiteID,
|
||||||
&i.Title,
|
&i.Title,
|
||||||
|
&i.PostTypeID,
|
||||||
&i.Body,
|
&i.Body,
|
||||||
&i.State,
|
&i.State,
|
||||||
&i.Props,
|
&i.Props,
|
||||||
&i.PostDate,
|
&i.PublishDate,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
@ -47,20 +49,22 @@ INSERT INTO posts (
|
||||||
body,
|
body,
|
||||||
state,
|
state,
|
||||||
props,
|
props,
|
||||||
post_date,
|
publish_date,
|
||||||
created_at
|
created_at,
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertPostParams struct {
|
type InsertPostParams struct {
|
||||||
SiteID int64
|
SiteID int64
|
||||||
Title pgtype.Text
|
Title pgtype.Text
|
||||||
Body string
|
Body string
|
||||||
State PostState
|
State PostState
|
||||||
Props []byte
|
Props []byte
|
||||||
PostDate pgtype.Timestamptz
|
PublishDate pgtype.Timestamptz
|
||||||
CreatedAt pgtype.Timestamp
|
CreatedAt pgtype.Timestamp
|
||||||
|
UpdatedAt pgtype.Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64, error) {
|
func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64, error) {
|
||||||
|
@ -70,8 +74,9 @@ func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64,
|
||||||
arg.Body,
|
arg.Body,
|
||||||
arg.State,
|
arg.State,
|
||||||
arg.Props,
|
arg.Props,
|
||||||
arg.PostDate,
|
arg.PublishDate,
|
||||||
arg.CreatedAt,
|
arg.CreatedAt,
|
||||||
|
arg.UpdatedAt,
|
||||||
)
|
)
|
||||||
var id int64
|
var id int64
|
||||||
err := row.Scan(&id)
|
err := row.Scan(&id)
|
||||||
|
@ -79,7 +84,7 @@ func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64,
|
||||||
}
|
}
|
||||||
|
|
||||||
const listPosts = `-- name: ListPosts :many
|
const listPosts = `-- name: ListPosts :many
|
||||||
SELECT id, site_id, title, body, state, props, post_date, created_at FROM posts WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25
|
SELECT id, site_id, title, post_type_id, body, state, props, publish_date, created_at, updated_at FROM posts WHERE site_id = $1 ORDER BY publish_date DESC LIMIT 25
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
||||||
|
@ -95,11 +100,13 @@ func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.SiteID,
|
&i.SiteID,
|
||||||
&i.Title,
|
&i.Title,
|
||||||
|
&i.PostTypeID,
|
||||||
&i.Body,
|
&i.Body,
|
||||||
&i.State,
|
&i.State,
|
||||||
&i.Props,
|
&i.Props,
|
||||||
&i.PostDate,
|
&i.PublishDate,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -112,20 +119,20 @@ func (q *Queries) ListPosts(ctx context.Context, siteID int64) ([]Post, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const listPublishablePosts = `-- name: ListPublishablePosts :many
|
const listPublishablePosts = `-- name: ListPublishablePosts :many
|
||||||
SELECT id, site_id, title, body, state, props, post_date, created_at
|
SELECT id, site_id, title, post_type_id, body, state, props, publish_date, created_at, updated_at
|
||||||
FROM posts
|
FROM posts
|
||||||
WHERE id > $1 AND site_id = $2 AND state = 'published' AND post_date <= $3
|
WHERE id > $1 AND site_id = $2 AND state = 'published' AND publish_date <= $3
|
||||||
ORDER BY id LIMIT 100
|
ORDER BY id LIMIT 100
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListPublishablePostsParams struct {
|
type ListPublishablePostsParams struct {
|
||||||
ID int64
|
ID int64
|
||||||
SiteID int64
|
SiteID int64
|
||||||
PostDate pgtype.Timestamptz
|
PublishDate pgtype.Timestamptz
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListPublishablePosts(ctx context.Context, arg ListPublishablePostsParams) ([]Post, error) {
|
func (q *Queries) ListPublishablePosts(ctx context.Context, arg ListPublishablePostsParams) ([]Post, error) {
|
||||||
rows, err := q.db.Query(ctx, listPublishablePosts, arg.ID, arg.SiteID, arg.PostDate)
|
rows, err := q.db.Query(ctx, listPublishablePosts, arg.ID, arg.SiteID, arg.PublishDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -137,11 +144,13 @@ func (q *Queries) ListPublishablePosts(ctx context.Context, arg ListPublishableP
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.SiteID,
|
&i.SiteID,
|
||||||
&i.Title,
|
&i.Title,
|
||||||
|
&i.PostTypeID,
|
||||||
&i.Body,
|
&i.Body,
|
||||||
&i.State,
|
&i.State,
|
||||||
&i.Props,
|
&i.Props,
|
||||||
&i.PostDate,
|
&i.PublishDate,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -160,19 +169,20 @@ UPDATE posts SET
|
||||||
body = $4,
|
body = $4,
|
||||||
state = $5,
|
state = $5,
|
||||||
props = $6,
|
props = $6,
|
||||||
post_date = $7
|
publish_date = $7,
|
||||||
-- updated_at = $7
|
updated_at = $8
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdatePostParams struct {
|
type UpdatePostParams struct {
|
||||||
ID int64
|
ID int64
|
||||||
SiteID int64
|
SiteID int64
|
||||||
Title pgtype.Text
|
Title pgtype.Text
|
||||||
Body string
|
Body string
|
||||||
State PostState
|
State PostState
|
||||||
Props []byte
|
Props []byte
|
||||||
PostDate pgtype.Timestamptz
|
PublishDate pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdatePost(ctx context.Context, arg UpdatePostParams) error {
|
func (q *Queries) UpdatePost(ctx context.Context, arg UpdatePostParams) error {
|
||||||
|
@ -183,7 +193,8 @@ func (q *Queries) UpdatePost(ctx context.Context, arg UpdatePostParams) error {
|
||||||
arg.Body,
|
arg.Body,
|
||||||
arg.State,
|
arg.State,
|
||||||
arg.Props,
|
arg.Props,
|
||||||
arg.PostDate,
|
arg.PublishDate,
|
||||||
|
arg.UpdatedAt,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -48,5 +48,5 @@ require (
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lmika.dev/pkg/modash v0.0.0-20250201221851-97d4b9b4a1ac // indirect
|
lmika.dev/pkg/modash v0.0.0-20250216001243-c73e50a0913d // indirect
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -99,3 +99,5 @@ lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1 h1:Seqp9vlIw3uJBL0V/eWIM
|
||||||
lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
lmika.dev/pkg/modash v0.0.0-20250127022145-5dcbffe270a1/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||||
lmika.dev/pkg/modash v0.0.0-20250201221851-97d4b9b4a1ac h1:i/C+DYDCVQTQHtv7w1O8m20RMez6YS9fUIlhAGjTZhU=
|
lmika.dev/pkg/modash v0.0.0-20250201221851-97d4b9b4a1ac h1:i/C+DYDCVQTQHtv7w1O8m20RMez6YS9fUIlhAGjTZhU=
|
||||||
lmika.dev/pkg/modash v0.0.0-20250201221851-97d4b9b4a1ac/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
lmika.dev/pkg/modash v0.0.0-20250201221851-97d4b9b4a1ac/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||||
|
lmika.dev/pkg/modash v0.0.0-20250216001243-c73e50a0913d h1:x5aMBOkCr4cjJyFmq+qJVUsByfffD9k56HYDx1yZSR4=
|
||||||
|
lmika.dev/pkg/modash v0.0.0-20250216001243-c73e50a0913d/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
|
||||||
|
|
123
handlers/page.go
Normal file
123
handlers/page.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"lmika.dev/lmika/hugo-cms/models"
|
||||||
|
"lmika.dev/lmika/hugo-cms/services/pages"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pages struct {
|
||||||
|
Svc *pages.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Pages) Index(c fiber.Ctx) error {
|
||||||
|
site := GetSite(c)
|
||||||
|
|
||||||
|
pages, err := h.Svc.ListPagesOfSite(c.Context(), site)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render("pages/index", fiber.Map{
|
||||||
|
"pages": pages,
|
||||||
|
}, "layouts/site")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Pages) New(c fiber.Ctx) error {
|
||||||
|
return c.Render("pages/edit", fiber.Map{
|
||||||
|
"page": models.Page{},
|
||||||
|
}, "layouts/site")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Pages) Create(c fiber.Ctx) error {
|
||||||
|
site := GetSite(c)
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Title string `json:"title" form:"title"`
|
||||||
|
Body string `json:"body" form:"body"`
|
||||||
|
}
|
||||||
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := h.Svc.Create(c.Context(), site, pages.NewPost{
|
||||||
|
Title: req.Title,
|
||||||
|
Body: req.Body,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Pages) Edit(c fiber.Ctx) error {
|
||||||
|
site := GetSite(c)
|
||||||
|
|
||||||
|
pageID := fiber.Params[int](c, "pageId")
|
||||||
|
if pageID == 0 {
|
||||||
|
return errors.New("pageId is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := h.Svc.GetPage(c.Context(), pageID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if page.SiteID != site.ID {
|
||||||
|
return fmt.Errorf("page id %v not equal to site id %v", pageID, site.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render("pages/edit", fiber.Map{
|
||||||
|
"page": page,
|
||||||
|
}, "layouts/site")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Pages) Update(c fiber.Ctx) error {
|
||||||
|
site := GetSite(c)
|
||||||
|
|
||||||
|
pageID := fiber.Params[int](c, "pageId")
|
||||||
|
if pageID == 0 {
|
||||||
|
return errors.New("pageId is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Title string `json:"title" form:"title"`
|
||||||
|
Body string `json:"body" form:"body"`
|
||||||
|
}
|
||||||
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.Svc.Update(c.Context(), site, int64(pageID), pages.NewPost{
|
||||||
|
Title: req.Title,
|
||||||
|
Body: req.Body,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Pages) Delete(c fiber.Ctx) error {
|
||||||
|
site := GetSite(c)
|
||||||
|
|
||||||
|
pageID := fiber.Params[int](c, "pageId")
|
||||||
|
if pageID == 0 {
|
||||||
|
return errors.New("pageID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.Svc.DeletePage(c.Context(), site, pageID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Select(c,
|
||||||
|
HTMX(func(c fiber.Ctx) error {
|
||||||
|
return c.Status(http.StatusOK).SendString("")
|
||||||
|
}),
|
||||||
|
Otherwise(func(c fiber.Ctx) error {
|
||||||
|
return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
16
main.go
16
main.go
|
@ -21,6 +21,7 @@ import (
|
||||||
"lmika.dev/lmika/hugo-cms/providers/netlify"
|
"lmika.dev/lmika/hugo-cms/providers/netlify"
|
||||||
"lmika.dev/lmika/hugo-cms/providers/themes"
|
"lmika.dev/lmika/hugo-cms/providers/themes"
|
||||||
"lmika.dev/lmika/hugo-cms/services/jobs"
|
"lmika.dev/lmika/hugo-cms/services/jobs"
|
||||||
|
"lmika.dev/lmika/hugo-cms/services/pages"
|
||||||
"lmika.dev/lmika/hugo-cms/services/posts"
|
"lmika.dev/lmika/hugo-cms/services/posts"
|
||||||
"lmika.dev/lmika/hugo-cms/services/sitebuilder"
|
"lmika.dev/lmika/hugo-cms/services/sitebuilder"
|
||||||
"lmika.dev/lmika/hugo-cms/services/sites"
|
"lmika.dev/lmika/hugo-cms/services/sites"
|
||||||
|
@ -71,7 +72,7 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hugoProvider, err := hugo.New(cfg.StagingDir(), cfg.ScratchDir())
|
hugoProvider, err := hugo.New(cfg.StagingDir(), cfg.BaseURL, cfg.PreviewDir(), cfg.ScratchDir())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -85,10 +86,12 @@ func main() {
|
||||||
|
|
||||||
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)
|
||||||
|
pageService := pages.New(dbp, siteBuilderService, jobService)
|
||||||
|
|
||||||
indexHandlers := handlers.IndexHandler{}
|
indexHandlers := handlers.IndexHandler{}
|
||||||
siteHandlers := handlers.Site{Site: siteService, Bus: bus}
|
siteHandlers := handlers.Site{Site: siteService, Bus: bus}
|
||||||
postHandlers := handlers.Post{Post: postService}
|
postHandlers := handlers.Post{Post: postService}
|
||||||
|
pageHandlers := handlers.Pages{Svc: pageService}
|
||||||
authHandlers := handlers.AuthHandler{UserService: userService}
|
authHandlers := handlers.AuthHandler{UserService: userService}
|
||||||
|
|
||||||
tmplEngine := html.NewFileSystem(http.FS(templates.FS), ".html")
|
tmplEngine := html.NewFileSystem(http.FS(templates.FS), ".html")
|
||||||
|
@ -126,6 +129,10 @@ func main() {
|
||||||
app.Post("/sites", siteHandlers.Create)
|
app.Post("/sites", siteHandlers.Create)
|
||||||
app.Get("/sites/:siteId", siteHandlers.Show)
|
app.Get("/sites/:siteId", siteHandlers.Show)
|
||||||
|
|
||||||
|
app.Use("/preview", static.New(cfg.PreviewDir(), static.Config{
|
||||||
|
Browse: true,
|
||||||
|
}))
|
||||||
|
|
||||||
sr := app.Group("/sites/:siteId")
|
sr := app.Group("/sites/:siteId")
|
||||||
sr.Use(siteHandlers.WithSite())
|
sr.Use(siteHandlers.WithSite())
|
||||||
sr.Post("/rebuild", siteHandlers.Rebuild)
|
sr.Post("/rebuild", siteHandlers.Rebuild)
|
||||||
|
@ -137,6 +144,13 @@ func main() {
|
||||||
sr.Post("/posts/:postId", postHandlers.Update)
|
sr.Post("/posts/:postId", postHandlers.Update)
|
||||||
sr.Delete("/posts/:postId", postHandlers.Delete)
|
sr.Delete("/posts/:postId", postHandlers.Delete)
|
||||||
|
|
||||||
|
sr.Get("/pages", pageHandlers.Index)
|
||||||
|
sr.Get("/pages/new", pageHandlers.New)
|
||||||
|
sr.Post("/pages", pageHandlers.Create)
|
||||||
|
sr.Get("/pages/:pageId", pageHandlers.Edit)
|
||||||
|
sr.Post("/pages/:pageId", pageHandlers.Update)
|
||||||
|
sr.Delete("/pages/:pageId", pageHandlers.Delete)
|
||||||
|
|
||||||
sr.Get("/settings", siteHandlers.Settings)
|
sr.Get("/settings", siteHandlers.Settings)
|
||||||
sr.Post("/settings", siteHandlers.SaveSettings)
|
sr.Post("/settings", siteHandlers.SaveSettings)
|
||||||
sr.Get("/sse", siteHandlers.SSE)
|
sr.Get("/sse", siteHandlers.SSE)
|
||||||
|
|
54
models/page.go
Normal file
54
models/page.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
RootBundleName = "_root"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NameProvenance encodes where the name came from, whether it was set by the user or autogenerated in some way
|
||||||
|
type NameProvenance int
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserNameProvenance NameProvenance = iota
|
||||||
|
TitleNameProvenance NameProvenance = iota
|
||||||
|
DateNameProvenance NameProvenance = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
type PageRole int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NormalPageRole PageRole = iota
|
||||||
|
IndexPageRole
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bundle struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
Name string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
ID int64
|
||||||
|
SiteID int64
|
||||||
|
BundleID int64
|
||||||
|
Name string
|
||||||
|
NameProvenance NameProvenance
|
||||||
|
Title string
|
||||||
|
Role PageRole
|
||||||
|
Body string
|
||||||
|
State PostState
|
||||||
|
PageTypeID int64
|
||||||
|
Props []byte
|
||||||
|
PublishDate time.Time
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type BundleInfo struct {
|
||||||
|
BundleID int64
|
||||||
|
PageCount int
|
||||||
|
IndexPageID int64
|
||||||
|
}
|
|
@ -10,13 +10,13 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Post struct {
|
type Post struct {
|
||||||
ID int64
|
ID int64
|
||||||
SiteID int64
|
SiteID int64
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
Title string
|
Title string
|
||||||
Body string
|
Body string
|
||||||
State PostState
|
State PostState
|
||||||
PostDate time.Time
|
PublishDate time.Time
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ type ThemeMeta struct {
|
||||||
// Indicates that this theme prefers posts have titles.
|
// Indicates that this theme prefers posts have titles.
|
||||||
PreferTitle bool
|
PreferTitle bool
|
||||||
|
|
||||||
// Content directory for "blog" posts
|
// Indicates that the theme doesn't automatically put titles on pages
|
||||||
PostDir string `json:"post_dir"`
|
AddTitleToPages bool
|
||||||
|
|
||||||
|
// Page bundle for "blog" posts
|
||||||
|
BlogPostBundle string `json:"post_dir"`
|
||||||
}
|
}
|
||||||
|
|
65
providers/db/bundles.go
Normal file
65
providers/db/bundles.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"lmika.dev/lmika/hugo-cms/gen/sqlc/dbq"
|
||||||
|
"lmika.dev/lmika/hugo-cms/models"
|
||||||
|
"lmika.dev/pkg/modash/momap"
|
||||||
|
"lmika.dev/pkg/modash/moslice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *DB) InsertBundle(ctx context.Context, bundle *models.Bundle) error {
|
||||||
|
id, err := db.q.InsertBundle(ctx, dbq.InsertBundleParams{
|
||||||
|
SiteID: bundle.SiteID,
|
||||||
|
Name: bundle.Name,
|
||||||
|
CreatedAt: pgtype.Timestamp{Time: bundle.CreatedAt, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bundle.ID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListBundles(ctx context.Context, siteID int64) ([]models.Bundle, error) {
|
||||||
|
res, err := db.q.ListBundles(ctx, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return moslice.Map(res, dbBundleToBundle), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetBundleWithID(ctx context.Context, id int64) (models.Bundle, error) {
|
||||||
|
res, err := db.q.GetBundleWithID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return models.Bundle{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbBundleToBundle(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetSiteBundleInfo(ctx context.Context, siteID int64) (map[int64]models.BundleInfo, error) {
|
||||||
|
res, err := db.q.GetSiteBundleInfo(ctx, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return momap.FromSlice(res, func(bi dbq.GetSiteBundleInfoRow) (int64, models.BundleInfo) {
|
||||||
|
return bi.BundleID, models.BundleInfo{
|
||||||
|
BundleID: bi.BundleID,
|
||||||
|
PageCount: int(bi.PageCount),
|
||||||
|
IndexPageID: bi.IndexPageID.Int64,
|
||||||
|
}
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbBundleToBundle(b dbq.Bundle) models.Bundle {
|
||||||
|
return models.Bundle{
|
||||||
|
ID: b.ID,
|
||||||
|
SiteID: b.SiteID,
|
||||||
|
Name: b.Name,
|
||||||
|
CreatedAt: b.CreatedAt.Time,
|
||||||
|
UpdatedAt: b.UpdatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
114
providers/db/page.go
Normal file
114
providers/db/page.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"lmika.dev/lmika/hugo-cms/gen/sqlc/dbq"
|
||||||
|
"lmika.dev/lmika/hugo-cms/models"
|
||||||
|
"lmika.dev/pkg/modash/momap"
|
||||||
|
"lmika.dev/pkg/modash/moslice"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nameProvenanceToDBNameProvenance = map[models.NameProvenance]dbq.PageNameProvenance{
|
||||||
|
models.UserNameProvenance: dbq.PageNameProvenanceUser,
|
||||||
|
models.TitleNameProvenance: dbq.PageNameProvenanceTitle,
|
||||||
|
models.DateNameProvenance: dbq.PageNameProvenanceDate,
|
||||||
|
}
|
||||||
|
var dbNameProvenanceToNameProvenance = momap.ReverseMap(nameProvenanceToDBNameProvenance)
|
||||||
|
|
||||||
|
var pageRoleToDBPageRole = map[models.PageRole]dbq.NullPageRole{
|
||||||
|
models.NormalPageRole: {},
|
||||||
|
models.IndexPageRole: {PageRole: dbq.PageRoleIndex, Valid: true},
|
||||||
|
}
|
||||||
|
var dbPageRoleToPageRole = momap.ReverseMap(pageRoleToDBPageRole)
|
||||||
|
|
||||||
|
func (db *DB) InsertPage(ctx context.Context, page *models.Page) error {
|
||||||
|
|
||||||
|
id, err := db.q.InsertPage(ctx, dbq.InsertPageParams{
|
||||||
|
SiteID: page.SiteID,
|
||||||
|
BundleID: page.BundleID,
|
||||||
|
Name: page.Name,
|
||||||
|
NameProvenance: nameProvenanceToDBNameProvenance[page.NameProvenance],
|
||||||
|
Role: pageRoleToDBPageRole[page.Role],
|
||||||
|
Title: pgtype.Text{String: page.Title, Valid: page.Title != ""},
|
||||||
|
Body: page.Body,
|
||||||
|
State: dbq.PostState(page.State),
|
||||||
|
Props: []byte(`{}`),
|
||||||
|
PublishDate: pgtype.Timestamptz{Time: page.PublishDate, Valid: !page.PublishDate.IsZero()},
|
||||||
|
CreatedAt: pgtype.Timestamp{Time: page.CreatedAt, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
page.ID = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdatePage(ctx context.Context, page *models.Page) error {
|
||||||
|
return db.q.UpdatePage(ctx, dbq.UpdatePageParams{
|
||||||
|
ID: page.ID,
|
||||||
|
SiteID: page.SiteID,
|
||||||
|
BundleID: page.BundleID,
|
||||||
|
Name: page.Name,
|
||||||
|
Role: pageRoleToDBPageRole[page.Role],
|
||||||
|
NameProvenance: nameProvenanceToDBNameProvenance[page.NameProvenance],
|
||||||
|
Title: pgtype.Text{String: page.Title, Valid: page.Title != ""},
|
||||||
|
Body: page.Body,
|
||||||
|
State: dbq.PostState(page.State),
|
||||||
|
Props: []byte(`{}`),
|
||||||
|
PublishDate: pgtype.Timestamptz{Time: page.PublishDate, Valid: true},
|
||||||
|
CreatedAt: pgtype.Timestamp{Time: page.CreatedAt, Valid: true},
|
||||||
|
UpdatedAt: pgtype.Timestamp{Time: page.UpdatedAt, Valid: true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListPagesOfSite(ctx context.Context, siteID int64) ([]models.Page, error) {
|
||||||
|
res, err := db.q.ListPages(ctx, siteID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return moslice.Map(res, dbPageToPage), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) ListPublishablePages(ctx context.Context, fromID, siteID int64) ([]models.Page, error) {
|
||||||
|
res, err := db.q.ListPublishablePages(ctx, dbq.ListPublishablePagesParams{
|
||||||
|
ID: fromID,
|
||||||
|
SiteID: siteID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return moslice.Map(res, dbPageToPage), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetPage(ctx context.Context, postID int64) (models.Page, error) {
|
||||||
|
res, err := db.q.GetPageWithID(ctx, postID)
|
||||||
|
if err != nil {
|
||||||
|
return models.Page{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbPageToPage(res), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeletePage(ctx context.Context, pageID int64) error {
|
||||||
|
|
||||||
|
return db.q.DeletePageWithID(ctx, pageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbPageToPage(p dbq.Page) models.Page {
|
||||||
|
return models.Page{
|
||||||
|
ID: p.ID,
|
||||||
|
SiteID: p.SiteID,
|
||||||
|
BundleID: p.BundleID,
|
||||||
|
Name: p.Name,
|
||||||
|
Role: dbPageRoleToPageRole[p.Role],
|
||||||
|
NameProvenance: dbNameProvenanceToNameProvenance[p.NameProvenance],
|
||||||
|
Title: p.Title.String,
|
||||||
|
Body: p.Body,
|
||||||
|
State: models.PostState(p.State),
|
||||||
|
PublishDate: p.PublishDate.Time,
|
||||||
|
CreatedAt: p.CreatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,9 +33,9 @@ func (db *DB) DeletePost(ctx context.Context, postID int64) error {
|
||||||
|
|
||||||
func (db *DB) ListPublishablePosts(ctx context.Context, fromID, siteID int64, now time.Time) ([]models.Post, error) {
|
func (db *DB) ListPublishablePosts(ctx context.Context, fromID, siteID int64, now time.Time) ([]models.Post, error) {
|
||||||
res, err := db.q.ListPublishablePosts(ctx, dbq.ListPublishablePostsParams{
|
res, err := db.q.ListPublishablePosts(ctx, dbq.ListPublishablePostsParams{
|
||||||
ID: fromID,
|
ID: fromID,
|
||||||
SiteID: siteID,
|
SiteID: siteID,
|
||||||
PostDate: pgtype.Timestamptz{Time: now, Valid: true},
|
PublishDate: pgtype.Timestamptz{Time: now, Valid: true},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -46,13 +46,14 @@ func (db *DB) ListPublishablePosts(ctx context.Context, fromID, siteID int64, no
|
||||||
|
|
||||||
func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
|
func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
|
||||||
res, err := db.q.InsertPost(ctx, dbq.InsertPostParams{
|
res, err := db.q.InsertPost(ctx, dbq.InsertPostParams{
|
||||||
SiteID: p.SiteID,
|
SiteID: p.SiteID,
|
||||||
Title: pgtype.Text{String: p.Title, Valid: p.Title != ""},
|
Title: pgtype.Text{String: p.Title, Valid: p.Title != ""},
|
||||||
Body: p.Body,
|
Body: p.Body,
|
||||||
State: dbq.PostState(p.State),
|
State: dbq.PostState(p.State),
|
||||||
Props: []byte(`{}`),
|
Props: []byte(`{}`),
|
||||||
PostDate: pgtype.Timestamptz{Time: p.PostDate, Valid: !p.PostDate.IsZero()},
|
PublishDate: pgtype.Timestamptz{Time: p.PublishDate, Valid: !p.PublishDate.IsZero()},
|
||||||
CreatedAt: pgtype.Timestamp{Time: p.CreatedAt, Valid: !p.CreatedAt.IsZero()},
|
CreatedAt: pgtype.Timestamp{Time: p.CreatedAt, Valid: !p.CreatedAt.IsZero()},
|
||||||
|
UpdatedAt: pgtype.Timestamp{Time: p.UpdatedAt, Valid: !p.UpdatedAt.IsZero()},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -64,25 +65,25 @@ func (db *DB) InsertPost(ctx context.Context, p *models.Post) error {
|
||||||
|
|
||||||
func (db *DB) UpdatePost(ctx context.Context, p *models.Post) error {
|
func (db *DB) UpdatePost(ctx context.Context, p *models.Post) error {
|
||||||
return db.q.UpdatePost(ctx, dbq.UpdatePostParams{
|
return db.q.UpdatePost(ctx, dbq.UpdatePostParams{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
SiteID: p.SiteID,
|
SiteID: p.SiteID,
|
||||||
Title: pgtype.Text{String: p.Title, Valid: p.Title != ""},
|
Title: pgtype.Text{String: p.Title, Valid: p.Title != ""},
|
||||||
Body: p.Body,
|
Body: p.Body,
|
||||||
State: dbq.PostState(p.State),
|
State: dbq.PostState(p.State),
|
||||||
Props: []byte(`{}`),
|
Props: []byte(`{}`),
|
||||||
PostDate: pgtype.Timestamptz{Time: p.PostDate, Valid: !p.PostDate.IsZero()},
|
PublishDate: pgtype.Timestamptz{Time: p.PublishDate, Valid: !p.PublishDate.IsZero()},
|
||||||
//CreatedAt: pgtype.Timestamp{Time: p.CreatedAt, Valid: !p.CreatedAt.IsZero()},
|
UpdatedAt: pgtype.Timestamp{Time: p.UpdatedAt, Valid: !p.UpdatedAt.IsZero()},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbPostToPost(p dbq.Post) models.Post {
|
func dbPostToPost(p dbq.Post) models.Post {
|
||||||
return models.Post{
|
return models.Post{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
SiteID: p.SiteID,
|
SiteID: p.SiteID,
|
||||||
Title: p.Title.String,
|
Title: p.Title.String,
|
||||||
Body: p.Body,
|
Body: p.Body,
|
||||||
State: models.PostState(p.State),
|
State: models.PostState(p.State),
|
||||||
PostDate: p.PostDate.Time,
|
PublishDate: p.PublishDate.Time,
|
||||||
CreatedAt: p.CreatedAt.Time,
|
CreatedAt: p.CreatedAt.Time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ type hugoConfig struct {
|
||||||
LanguageCode string `yaml:"languageCode"`
|
LanguageCode string `yaml:"languageCode"`
|
||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
Theme string `yaml:"theme"`
|
Theme string `yaml:"theme"`
|
||||||
|
CanonifyURLs bool `yaml:"canonifyURLs,omitempty"`
|
||||||
|
|
||||||
Markup hugoConfigMarkup `yaml:"markup"`
|
Markup hugoConfigMarkup `yaml:"markup"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,30 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"lmika.dev/lmika/hugo-cms/models"
|
"lmika.dev/lmika/hugo-cms/models"
|
||||||
"log"
|
"log"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
stagingDir string
|
stagingDir string
|
||||||
scratchDir string
|
previewDir string
|
||||||
|
previewBaseURL *url.URL
|
||||||
|
scratchDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(stagingDir, scratchDir string) (*Provider, error) {
|
func New(stagingDir, previewBaseURL, previewDir, scratchDir string) (*Provider, error) {
|
||||||
|
baseURL, err := url.Parse(previewBaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Provider{
|
return &Provider{
|
||||||
stagingDir: stagingDir,
|
stagingDir: stagingDir,
|
||||||
scratchDir: scratchDir,
|
previewBaseURL: baseURL,
|
||||||
|
previewDir: previewDir,
|
||||||
|
scratchDir: scratchDir,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,38 +57,53 @@ 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) {
|
func (p *Provider) PreviewSite(ctx context.Context, site models.Site) (outDir string, err error) {
|
||||||
if err := os.MkdirAll(p.scratchDir, 0755); err != nil {
|
previewURL, err := p.previewBaseURL.Parse("preview/" + site.Name)
|
||||||
return "", nil, err
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
previewTarget := models.PublishTarget{
|
||||||
|
URL: previewURL.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
outDir, err = os.MkdirTemp(p.scratchDir, site.Name+"-*")
|
return p.publishSiteAt(ctx, p.previewDir, site, previewTarget, "hugoPreview.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) PublishSite(ctx context.Context, site models.Site, target models.PublishTarget) (outDir string, err error) {
|
||||||
|
return p.publishSiteAt(ctx, p.scratchDir, site, target, "hugo.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) publishSiteAt(ctx context.Context, dir string, site models.Site, target models.PublishTarget, configFile string) (outDir string, err error) {
|
||||||
|
baseSiteDir, err := filepath.Abs(p.SiteStagingDir(site, BaseSiteDir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", err
|
||||||
}
|
|
||||||
clean = func() {
|
|
||||||
os.RemoveAll(outDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outDir, err = filepath.Abs(outDir)
|
outDir, err = filepath.Abs(filepath.Join(dir, site.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "hugo",
|
cmd := exec.CommandContext(ctx, "hugo",
|
||||||
"--source", p.SiteStagingDir(site, BaseSiteDir),
|
"--source", baseSiteDir,
|
||||||
"--destination", outDir,
|
"--destination", outDir,
|
||||||
|
"--quiet",
|
||||||
|
"--config", filepath.Join(baseSiteDir, configFile),
|
||||||
"--baseURL", target.URL)
|
"--baseURL", target.URL)
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return "", clean, err
|
return "", err
|
||||||
}
|
}
|
||||||
return outDir, clean, nil
|
return outDir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) ReconfigureSite(ctx context.Context, site models.Site) error {
|
func (p *Provider) ReconfigureSite(ctx context.Context, isPreviewConfig bool, configBase string, site models.Site) error {
|
||||||
hugoCfg := hugoConfig{
|
hugoCfg := hugoConfig{
|
||||||
Title: site.Title,
|
Title: site.Title,
|
||||||
LanguageCode: "en",
|
LanguageCode: "en",
|
||||||
|
@ -92,17 +117,21 @@ func (p *Provider) ReconfigureSite(ctx context.Context, site models.Site) error
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isPreviewConfig {
|
||||||
|
hugoCfg.CanonifyURLs = true
|
||||||
|
}
|
||||||
|
|
||||||
ymlBytes, err := yaml.Marshal(hugoCfg)
|
ymlBytes, err := yaml.Marshal(hugoCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), "hugo.yaml"), ymlBytes, 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), configBase+".yaml"), ymlBytes, 0644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), "hugo.toml")); err == nil {
|
if _, err := os.Stat(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), configBase+".toml")); err == nil {
|
||||||
if err := os.Remove(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), "hugo.toml")); err != nil {
|
if err := os.Remove(filepath.Join(p.SiteStagingDir(site, BaseSiteDir), configBase+".toml")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,24 +4,24 @@ import "lmika.dev/lmika/hugo-cms/models"
|
||||||
|
|
||||||
var themes = []models.ThemeMeta{
|
var themes = []models.ThemeMeta{
|
||||||
{
|
{
|
||||||
ID: "bear",
|
ID: "bear",
|
||||||
Name: "Bear",
|
Name: "Bear",
|
||||||
URL: "https://github.com/janraasch/hugo-bearblog",
|
URL: "https://github.com/janraasch/hugo-bearblog",
|
||||||
PreferTitle: true,
|
PreferTitle: true,
|
||||||
PostDir: "blog",
|
BlogPostBundle: "blog",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "terminal",
|
ID: "terminal",
|
||||||
Name: "Terminal",
|
Name: "Terminal",
|
||||||
URL: "https://github.com/panr/hugo-theme-terminal",
|
URL: "https://github.com/panr/hugo-theme-terminal",
|
||||||
PreferTitle: true,
|
PreferTitle: true,
|
||||||
PostDir: "posts",
|
BlogPostBundle: "posts",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "yingyang",
|
ID: "yingyang",
|
||||||
Name: "Yingyang",
|
Name: "Yingyang",
|
||||||
URL: "https://github.com/joway/hugo-theme-yinyang",
|
URL: "https://github.com/joway/hugo-theme-yinyang",
|
||||||
PreferTitle: true,
|
PreferTitle: true,
|
||||||
PostDir: "posts",
|
BlogPostBundle: "posts",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
205
services/pages/services.go
Normal file
205
services/pages/services.go
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
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
|
||||||
|
} else if post.SiteID != site.ID {
|
||||||
|
return errors.New("page not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.DeletePage(ctx, int64(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.jobs.Queue(ctx, s.sb.RebuildSiteContent(site, site))
|
||||||
|
}
|
||||||
|
|
||||||
|
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: name,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.jobs.Queue(ctx, s.sb.RebuildSiteContent(site, site)); 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldPage := page
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// A content only change involves rewriting the file content. Anything else is a structural change
|
||||||
|
// that will need rewriting of all the page content.
|
||||||
|
contentOnlyChange := page.Name == oldPage.Name && page.Role == oldPage.Role
|
||||||
|
|
||||||
|
if contentOnlyChange {
|
||||||
|
if err := s.jobs.Queue(ctx, s.sb.WritePage(site, bundle, page)); err != nil {
|
||||||
|
return models.Page{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := s.jobs.Queue(ctx, s.sb.RebuildSiteContent(site, site)); 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 nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
func (s *Service) Create(ctx context.Context, site models.Site, req NewPost) (models.Post, error) {
|
||||||
post := models.Post{
|
post := models.Post{
|
||||||
SiteID: site.ID,
|
SiteID: site.ID,
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Body: req.Body,
|
Body: req.Body,
|
||||||
State: models.PostStatePublished,
|
State: models.PostStatePublished,
|
||||||
PostDate: time.Now(),
|
PublishDate: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Save(ctx, site, &post); err != nil {
|
if err := s.Save(ctx, site, &post); err != nil {
|
||||||
|
|
146
services/sitebuilder/pages.go
Normal file
146
services/sitebuilder/pages.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleInfo, err := s.db.GetSiteBundleInfo(ctx, site.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.writePage(pageBuildInfo{
|
||||||
|
site: site,
|
||||||
|
themeMeta: themeMeta,
|
||||||
|
bundle: bundle,
|
||||||
|
bundleInfo: bundleInfo,
|
||||||
|
}, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
themeMeta, ok := s.themes.Lookup(site.Theme)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("theme %s not found in themes", site.Theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleInfo, err := s.db.GetSiteBundleInfo(ctx, site.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
postFilename := s.pageFilename(pageBuildInfo{
|
||||||
|
site: site,
|
||||||
|
themeMeta: themeMeta,
|
||||||
|
bundle: bundle,
|
||||||
|
bundleInfo: bundleInfo,
|
||||||
|
}, 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 })
|
||||||
|
|
||||||
|
bundleInfo, err := s.db.GetSiteBundleInfo(ctx, site.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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(pageBuildInfo{
|
||||||
|
site: site,
|
||||||
|
themeMeta: themeMeta,
|
||||||
|
bundle: bundlesByID[page.BundleID],
|
||||||
|
bundleInfo: bundleInfo,
|
||||||
|
}, page); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startId = pages[len(pages)-1].ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) writePage(bi pageBuildInfo, page models.Page) error {
|
||||||
|
postFilename := s.pageFilename(bi, page)
|
||||||
|
|
||||||
|
frontMatter := map[string]any{
|
||||||
|
"date": page.PublishDate.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
if page.Title != "" {
|
||||||
|
frontMatter["title"] = page.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.writeMarkdownFile(postFilename, frontMatter, page.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pageBuildInfo struct {
|
||||||
|
site models.Site
|
||||||
|
themeMeta models.ThemeMeta
|
||||||
|
bundle models.Bundle
|
||||||
|
bundleInfo map[int64]models.BundleInfo
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"lmika.dev/lmika/hugo-cms/models"
|
"lmika.dev/lmika/hugo-cms/models"
|
||||||
"lmika.dev/lmika/hugo-cms/providers/hugo"
|
"lmika.dev/lmika/hugo-cms/providers/hugo"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
@ -104,19 +103,21 @@ func (s *Service) writePost(site models.Site, post models.Post) error {
|
||||||
|
|
||||||
postFilename := s.postFilename(site, themeMeta, post)
|
postFilename := s.postFilename(site, themeMeta, post)
|
||||||
|
|
||||||
log.Printf(" .. post %v", postFilename)
|
frontMatter := map[string]any{
|
||||||
|
"date": post.PublishDate.Format(time.RFC3339),
|
||||||
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 != "" {
|
if post.Title != "" {
|
||||||
frontMatter["title"] = post.Title
|
frontMatter["title"] = post.Title
|
||||||
} else if themeMeta.PreferTitle {
|
} 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)
|
fmBytes, err := yaml.Marshal(frontMatter)
|
||||||
|
@ -124,7 +125,7 @@ func (s *Service) writePost(site models.Site, post models.Post) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Create(postFilename)
|
f, err := os.Create(outFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -139,7 +140,7 @@ func (s *Service) writePost(site models.Site, post models.Post) error {
|
||||||
if _, err := f.WriteString("---\n"); err != nil {
|
if _, err := f.WriteString("---\n"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := f.WriteString(post.Body); err != nil {
|
if _, err := f.WriteString(body); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,5 +148,35 @@ 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 {
|
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(bi pageBuildInfo, page models.Page) string {
|
||||||
|
isIndex := false
|
||||||
|
isLeafBundle := true
|
||||||
|
|
||||||
|
thisBundleInfo := bi.bundleInfo[bi.bundle.ID]
|
||||||
|
if thisBundleInfo.PageCount > 1 || bi.bundle.Name == models.RootBundleName {
|
||||||
|
isLeafBundle = false
|
||||||
|
isIndex = thisBundleInfo.IndexPageID == page.ID
|
||||||
|
} else {
|
||||||
|
isIndex = true
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleDir := ""
|
||||||
|
if bi.bundle.Name != models.RootBundleName {
|
||||||
|
bundleDir = bi.bundle.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
pageName := page.Name
|
||||||
|
if isIndex {
|
||||||
|
if isLeafBundle {
|
||||||
|
pageName = "index"
|
||||||
|
} else {
|
||||||
|
pageName = "_index"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pageName += ".md"
|
||||||
|
return filepath.Join(s.hugo.SiteStagingDir(bi.site, hugo.ContentSiteDir), bundleDir, pageName)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,17 @@ func (s *Service) Publish(site models.Site) models.Job {
|
||||||
Do: func(ctx context.Context) error {
|
Do: func(ctx context.Context) error {
|
||||||
s.signalSiteBuildingStarted(ctx, site)
|
s.signalSiteBuildingStarted(ctx, site)
|
||||||
defer s.signalSiteBuildingFinished(ctx, site)
|
defer s.signalSiteBuildingFinished(ctx, site)
|
||||||
|
|
||||||
return s.publish(ctx, site)
|
return s.publish(ctx, site)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) publish(ctx context.Context, site models.Site) error {
|
func (s *Service) publish(ctx context.Context, site models.Site) error {
|
||||||
|
if _, err := s.hugo.PreviewSite(ctx, site); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
targets, err := s.db.GetPublishTargets(ctx, site.ID)
|
targets, err := s.db.GetPublishTargets(ctx, site.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -31,14 +35,7 @@ func (s *Service) publish(ctx context.Context, site models.Site) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) publishTarget(ctx context.Context, site models.Site, target models.PublishTarget) error {
|
func (s *Service) publishTarget(ctx context.Context, site models.Site, target models.PublishTarget) error {
|
||||||
outDir, cleanFn, err := s.hugo.PublishSite(ctx, site, target)
|
outDir, err := s.hugo.PublishSite(ctx, site, target)
|
||||||
//defer func() {
|
|
||||||
// if cleanFn != nil {
|
|
||||||
// cleanFn()
|
|
||||||
// }
|
|
||||||
//}()
|
|
||||||
_ = cleanFn
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,17 @@ func (s *Service) RebuildSite(oldSite, newSite models.Site) models.Job {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) RebuildSiteContent(oldSite, newSite models.Site) models.Job {
|
||||||
|
return models.Job{
|
||||||
|
Do: func(ctx context.Context) error {
|
||||||
|
s.signalSiteBuildingStarted(ctx, newSite)
|
||||||
|
defer s.signalSiteBuildingFinished(ctx, newSite)
|
||||||
|
|
||||||
|
return s.rebuildContent(ctx, oldSite, newSite)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) rebuildSite(ctx context.Context, oldSite, newSite models.Site) error {
|
func (s *Service) rebuildSite(ctx context.Context, oldSite, newSite models.Site) error {
|
||||||
// Teardown the existing site
|
// Teardown the existing site
|
||||||
siteDir := s.hugo.SiteStagingDir(oldSite, hugo.BaseSiteDir)
|
siteDir := s.hugo.SiteStagingDir(oldSite, hugo.BaseSiteDir)
|
||||||
|
@ -75,13 +86,39 @@ func (s *Service) rebuildSite(ctx context.Context, oldSite, newSite models.Site)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.writeAllPosts(ctx, newSite); err != nil {
|
if err := s.writeAllContent(ctx, newSite); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.publish(ctx, newSite)
|
return s.publish(ctx, newSite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) rebuildContent(ctx context.Context, oldSite, newSite models.Site) error {
|
||||||
|
// Teardown the existing site
|
||||||
|
siteDir := s.hugo.SiteStagingDir(oldSite, hugo.ContentSiteDir)
|
||||||
|
if err := os.RemoveAll(siteDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.writeAllContent(ctx, newSite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.publish(ctx, newSite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) writeAllContent(ctx context.Context, newSite models.Site) error {
|
||||||
|
if err := s.writeAllPosts(ctx, newSite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.writeAllPages(ctx, newSite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) fullRebuildNecessary(ctx context.Context, site models.Site) (bool, error) {
|
func (s *Service) fullRebuildNecessary(ctx context.Context, site models.Site) (bool, error) {
|
||||||
dirsMustExists := []string{
|
dirsMustExists := []string{
|
||||||
s.hugo.SiteStagingDir(site, hugo.BaseSiteDir),
|
s.hugo.SiteStagingDir(site, hugo.BaseSiteDir),
|
||||||
|
@ -135,9 +172,14 @@ func (s *Service) createSite(ctx context.Context, site models.Site) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.hugo.ReconfigureSite(ctx, site); err != nil {
|
if err := s.hugo.ReconfigureSite(ctx, false, "hugo", site); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.hugo.ReconfigureSite(ctx, true, "hugoPreview", site); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
services/sites/create.go
Normal file
56
services/sites/create.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
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,
|
||||||
|
Role: models.IndexPageRole,
|
||||||
|
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)
|
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 {
|
func (s *Service) SaveSettings(ctx context.Context, site models.Site, newSettings NewSettings) error {
|
||||||
_, ok := s.themes.Lookup(newSettings.SiteTheme)
|
_, ok := s.themes.Lookup(newSettings.SiteTheme)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -82,26 +61,28 @@ func (s *Service) SaveSettings(ctx context.Context, site models.Site, newSetting
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pubTarget, err := s.db.GetPublishTargetBySiteRole(ctx, newSite.ID, models.TargetRoleProduction)
|
if newSettings.TargetRef != "" && newSettings.TargetURL != "" {
|
||||||
if err == nil {
|
pubTarget, err := s.db.GetPublishTargetBySiteRole(ctx, newSite.ID, models.TargetRoleProduction)
|
||||||
pubTarget.TargetRef = newSettings.TargetRef
|
if err == nil {
|
||||||
pubTarget.URL = newSettings.TargetURL
|
pubTarget.TargetRef = newSettings.TargetRef
|
||||||
if err := s.db.UpdatePublishTarget(ctx, pubTarget); err != nil {
|
pubTarget.URL = newSettings.TargetURL
|
||||||
|
if err := s.db.UpdatePublishTarget(ctx, pubTarget); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
pubTarget = models.PublishTarget{
|
||||||
|
SiteID: newSite.ID,
|
||||||
|
Role: models.TargetRoleProduction,
|
||||||
|
Type: models.TargetTypeNetlify,
|
||||||
|
URL: newSettings.TargetURL,
|
||||||
|
TargetRef: newSettings.TargetRef,
|
||||||
|
}
|
||||||
|
if err := s.db.InsertPublishTarget(ctx, &pubTarget); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
pubTarget = models.PublishTarget{
|
|
||||||
SiteID: newSite.ID,
|
|
||||||
Role: models.TargetRoleProduction,
|
|
||||||
Type: models.TargetTypeNetlify,
|
|
||||||
URL: newSettings.TargetURL,
|
|
||||||
TargetRef: newSettings.TargetRef,
|
|
||||||
}
|
|
||||||
if err := s.db.InsertPublishTarget(ctx, &pubTarget); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.jobs.Queue(ctx, s.sb.RebuildSite(site, newSite))
|
return s.jobs.Queue(ctx, s.sb.RebuildSite(site, newSite))
|
||||||
|
|
21
sql/queries/bundles.sql
Normal file
21
sql/queries/bundles.sql
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
-- name: InsertBundle :one
|
||||||
|
INSERT INTO bundles (
|
||||||
|
site_id,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES ($1, $2, $3, $3) RETURNING id;
|
||||||
|
|
||||||
|
-- name: ListBundles :many
|
||||||
|
SELECT * FROM bundles WHERE site_id = $1;
|
||||||
|
|
||||||
|
-- name: GetBundleWithID :one
|
||||||
|
SELECT * FROM bundles WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetSiteBundleInfo :many
|
||||||
|
WITH page_counts AS (
|
||||||
|
SELECT b.bundle_id, count(*) AS page_count FROM pages b WHERE b.site_id = $1 GROUP BY bundle_id
|
||||||
|
), index_pages AS (
|
||||||
|
SELECT p.id AS index_page_id, p.bundle_id FROM pages p WHERE p.site_id = $1 AND p.role = 'index'
|
||||||
|
)
|
||||||
|
SELECT b.bundle_id, b.page_count, p.index_page_id FROM page_counts b LEFT OUTER JOIN index_pages p ON b.bundle_id = p.bundle_id;
|
49
sql/queries/pages.sql
Normal file
49
sql/queries/pages.sql
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
-- name: InsertPage :one
|
||||||
|
INSERT INTO pages (
|
||||||
|
site_id,
|
||||||
|
bundle_id,
|
||||||
|
name,
|
||||||
|
name_provenance,
|
||||||
|
title,
|
||||||
|
post_type_id,
|
||||||
|
body,
|
||||||
|
state,
|
||||||
|
props,
|
||||||
|
role,
|
||||||
|
publish_date,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $12)
|
||||||
|
RETURNING id;
|
||||||
|
|
||||||
|
-- name: UpdatePage :exec
|
||||||
|
UPDATE pages SET
|
||||||
|
site_id = $2,
|
||||||
|
bundle_id = $3,
|
||||||
|
name = $4,
|
||||||
|
name_provenance = $5,
|
||||||
|
title = $6,
|
||||||
|
post_type_id = $7,
|
||||||
|
role = $8,
|
||||||
|
body = $9,
|
||||||
|
state = $10,
|
||||||
|
props = $11,
|
||||||
|
publish_date = $12,
|
||||||
|
created_at = $13,
|
||||||
|
updated_at = $14
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListPublishablePages :many
|
||||||
|
SELECT *
|
||||||
|
FROM pages
|
||||||
|
WHERE id > $1 AND site_id = $2 AND state = 'published'
|
||||||
|
ORDER BY id LIMIT 100;
|
||||||
|
|
||||||
|
-- name: ListPages :many
|
||||||
|
SELECT * FROM pages WHERE site_id = $1 ORDER BY name ASC LIMIT 25;
|
||||||
|
|
||||||
|
-- name: GetPageWithID :one
|
||||||
|
SELECT * FROM pages WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: DeletePageWithID :exec
|
||||||
|
DELETE FROM pages WHERE id = $1;
|
|
@ -1,5 +1,5 @@
|
||||||
-- name: ListPosts :many
|
-- name: ListPosts :many
|
||||||
SELECT * FROM posts WHERE site_id = $1 ORDER BY post_date DESC LIMIT 25;
|
SELECT * FROM posts WHERE site_id = $1 ORDER BY publish_date DESC LIMIT 25;
|
||||||
|
|
||||||
-- name: GetPostWithID :one
|
-- name: GetPostWithID :one
|
||||||
SELECT * FROM posts WHERE id = $1 LIMIT 1;
|
SELECT * FROM posts WHERE id = $1 LIMIT 1;
|
||||||
|
@ -7,7 +7,7 @@ SELECT * FROM posts WHERE id = $1 LIMIT 1;
|
||||||
-- name: ListPublishablePosts :many
|
-- name: ListPublishablePosts :many
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM posts
|
FROM posts
|
||||||
WHERE id > $1 AND site_id = $2 AND state = 'published' AND post_date <= $3
|
WHERE id > $1 AND site_id = $2 AND state = 'published' AND publish_date <= $3
|
||||||
ORDER BY id LIMIT 100;
|
ORDER BY id LIMIT 100;
|
||||||
|
|
||||||
-- name: InsertPost :one
|
-- name: InsertPost :one
|
||||||
|
@ -17,9 +17,10 @@ INSERT INTO posts (
|
||||||
body,
|
body,
|
||||||
state,
|
state,
|
||||||
props,
|
props,
|
||||||
post_date,
|
publish_date,
|
||||||
created_at
|
created_at,
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id;
|
RETURNING id;
|
||||||
|
|
||||||
-- name: UpdatePost :exec
|
-- name: UpdatePost :exec
|
||||||
|
@ -29,8 +30,8 @@ UPDATE posts SET
|
||||||
body = $4,
|
body = $4,
|
||||||
state = $5,
|
state = $5,
|
||||||
props = $6,
|
props = $6,
|
||||||
post_date = $7
|
publish_date = $7,
|
||||||
-- updated_at = $7
|
updated_at = $8
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: DeletePost :exec
|
-- name: DeletePost :exec
|
||||||
|
|
|
@ -3,6 +3,10 @@ CREATE TYPE post_state AS ENUM (
|
||||||
'published'
|
'published'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TYPE post_format AS ENUM (
|
||||||
|
'markdown'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TYPE target_role AS ENUM (
|
CREATE TYPE target_role AS ENUM (
|
||||||
'production'
|
'production'
|
||||||
);
|
);
|
||||||
|
@ -11,6 +15,16 @@ CREATE TYPE target_type AS ENUM (
|
||||||
'netlify'
|
'netlify'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TYPE page_name_provenance AS ENUM (
|
||||||
|
'user',
|
||||||
|
'title',
|
||||||
|
'date'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE page_role AS ENUM (
|
||||||
|
'index'
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
email TEXT NOT NULL UNIQUE,
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
@ -28,19 +42,67 @@ CREATE TABLE sites (
|
||||||
FOREIGN KEY (owner_user_id) REFERENCES users (id)
|
FOREIGN KEY (owner_user_id) REFERENCES users (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE posts (
|
-- Post role is used to describe a specific kind of post, such as a link.
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
-- When set, it specifies the layout to use for the page
|
||||||
site_id BIGINT NOT NULL,
|
CREATE TABLE post_types (
|
||||||
title TEXT,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
body TEXT NOT NULL,
|
site_id BIGINT NOT NULL,
|
||||||
state post_state NOT NULL,
|
layout_name TEXT NOT NULL,
|
||||||
props JSON NOT NULL,
|
|
||||||
post_date TIMESTAMP WITH TIME ZONE,
|
|
||||||
created_at TIMESTAMP NOT NULL,
|
|
||||||
|
|
||||||
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
|
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
|
site_id BIGINT NOT NULL,
|
||||||
|
title TEXT,
|
||||||
|
post_type_id BIGINT,
|
||||||
|
format post_format NOT NULL DEFAULT 'markdown',
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
state post_state NOT NULL,
|
||||||
|
props JSON NOT NULL,
|
||||||
|
publish_date TIMESTAMP WITH TIME ZONE,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (post_type_id) REFERENCES post_types (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE bundles (
|
||||||
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
|
site_id BIGINT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE pages (
|
||||||
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
|
site_id BIGINT NOT NULL,
|
||||||
|
bundle_id BIGINT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
name_provenance page_name_provenance NOT NULL,
|
||||||
|
format post_format NOT NULL DEFAULT 'markdown',
|
||||||
|
title TEXT,
|
||||||
|
post_type_id BIGINT,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
state post_state NOT NULL,
|
||||||
|
props JSON NOT NULL,
|
||||||
|
role page_role,
|
||||||
|
publish_date TIMESTAMP WITH TIME ZONE,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE (bundle_id, name),
|
||||||
|
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (post_type_id) REFERENCES post_types (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (bundle_id) REFERENCES sites (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX page_bundle_id_role ON pages (bundle_id, role) WHERE (role is NOT null);
|
||||||
|
|
||||||
CREATE TABLE publish_targets (
|
CREATE TABLE publish_targets (
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
site_id BIGINT NOT NULL,
|
site_id BIGINT NOT NULL,
|
||||||
|
|
|
@ -6,5 +6,6 @@ import "embed"
|
||||||
//go:embed auth/*.html
|
//go:embed auth/*.html
|
||||||
//go:embed layouts/*.html
|
//go:embed layouts/*.html
|
||||||
//go:embed posts/*.html
|
//go:embed posts/*.html
|
||||||
|
//go:embed pages/*.html
|
||||||
//go:embed sites/*.html
|
//go:embed sites/*.html
|
||||||
var FS embed.FS
|
var FS embed.FS
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<h1>Hugo CMS</h1>
|
<h1>Hugo CMS</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<span>{{.site.Title}}</span>
|
<span>{{.site.Title}}</span>
|
||||||
|
<a href="/preview/{{.site.Name}}" target="_blank">Preview</a>
|
||||||
{{ if .prodTarget }}
|
{{ if .prodTarget }}
|
||||||
<a href="{{.prodTarget.URL}}" target="_blank">Visit</a>
|
<a href="{{.prodTarget.URL}}" target="_blank">Visit</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
13
templates/pages/edit.html
Normal file
13
templates/pages/edit.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{{- $postTarget := printf "/sites/%v/pages" .site.ID -}}
|
||||||
|
{{- if (ne .page.ID 0) -}}
|
||||||
|
{{- $postTarget = printf "/sites/%v/pages/%v" .site.ID .page.ID -}}
|
||||||
|
{{- end -}}
|
||||||
|
<form method="post" action="{{$postTarget}}" class="post-form">
|
||||||
|
<input name="title" placeholder="Title" value="{{.page.Title}}">
|
||||||
|
|
||||||
|
<textarea name="body">{{.page.Body}}</textarea>
|
||||||
|
|
||||||
|
<div class="bottom-bar">
|
||||||
|
<input type="submit" value="Post">
|
||||||
|
</div>
|
||||||
|
</form>
|
20
templates/pages/index.html
Normal file
20
templates/pages/index.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<div>
|
||||||
|
<a href="/sites/{{.site.ID}}/pages/new">New Page</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{range .pages}}
|
||||||
|
<div class="post">
|
||||||
|
{{if .Title}}
|
||||||
|
<h3>{{.Title}}</h3>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{.Body | markdown}}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/sites/{{$.site.ID}}/pages/{{.ID}}">Edit</a> |
|
||||||
|
<a hx-delete="/sites/{{$.site.ID}}/pages/{{.ID}}" hx-confirm="Delete page?" hx-target="closest .post" href="#">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p>No pages yet</p>
|
||||||
|
{{end}}
|
Loading…
Reference in a new issue