Started working on the frontend

- Added the new post frontend
- Hooked up publishing of posts to the site publisher
- Added an site exporter as a publishing target
This commit is contained in:
Leon Mika 2026-02-21 10:22:10 +11:00
parent a59008b3e8
commit e77cac2fd5
40 changed files with 1427 additions and 84 deletions

View file

@ -3,8 +3,10 @@ package models
import "context"
type userKeyType struct{}
type siteKeyType struct{}
var userKey = userKeyType{}
var siteKey = userKeyType{}
func WithUser(ctx context.Context, user User) context.Context {
return context.WithValue(ctx, userKey, user)
@ -14,3 +16,12 @@ func GetUser(ctx context.Context) (User, bool) {
user, ok := ctx.Value(userKey).(User)
return user, ok
}
func WithSite(ctx context.Context, site Site) context.Context {
return context.WithValue(ctx, siteKey, site)
}
func GetSite(ctx context.Context) (Site, bool) {
site, ok := ctx.Value(siteKey).(Site)
return site, ok
}

View file

@ -4,3 +4,5 @@ import "emperror.dev/errors"
var UserRequiredError = errors.New("user required")
var PermissionError = errors.New("permission denied")
var NotFoundError = errors.New("not found")
var SiteRequiredError = errors.New("site required")

8
models/ids.go Normal file
View file

@ -0,0 +1,8 @@
package models
import "github.com/matoous/go-nanoid/v2"
func NewNanoID() string {
id, _ := gonanoid.New(12)
return id
}

34
models/ids_test.go Normal file
View file

@ -0,0 +1,34 @@
package models
import (
"testing"
)
func TestNewNanoID(t *testing.T) {
id := NewNanoID()
if len(id) != 12 {
t.Errorf("Expected ID length of 12, got %d", len(id))
}
if id == "" {
t.Error("Expected non-empty ID")
}
}
func TestNewNanoID_Uniqueness(t *testing.T) {
ids := make(map[string]bool)
iterations := 1000
for i := 0; i < iterations; i++ {
id := NewNanoID()
if ids[id] {
t.Errorf("Duplicate ID generated: %s", id)
}
ids[id] = true
}
if len(ids) != iterations {
t.Errorf("Expected %d unique IDs, got %d", iterations, len(ids))
}
}

View file

@ -1,6 +1,12 @@
package models
import "time"
import (
"bufio"
"fmt"
"strings"
"time"
"unicode"
)
type Post struct {
ID int64
@ -12,3 +18,61 @@ type Post struct {
CreatedAt time.Time
PublishedAt time.Time
}
func (p *Post) BestSlug() string {
if p.Slug != "" {
return p.Slug
}
bestDateToUse := p.PublishedAt
if bestDateToUse.IsZero() {
bestDateToUse = p.CreatedAt
}
slugPath := firstNWords(p.Title, 3, wordForSlug)
if slugPath == "" {
slugPath = firstNWords(p.Body, 3, wordForSlug)
}
if slugPath != "" {
slugPath = strings.Replace(strings.ToLower(slugPath), " ", "-", -1)
} else {
slugPath = p.GUID
if slugPath == "" {
slugPath = bestDateToUse.Format("150405")
}
}
datePart := fmt.Sprintf("%04d/%02d/%02d", bestDateToUse.Year(), bestDateToUse.Month(), bestDateToUse.Day())
return fmt.Sprintf("/%s/%s", datePart, slugPath)
}
func wordForSlug(word string) string {
var sb strings.Builder
for _, c := range word {
if unicode.IsLetter(c) || unicode.IsNumber(c) {
sb.WriteRune(c)
}
}
return sb.String()
}
func firstNWords(s string, n int, keepWord func(word string) string) string {
if n == 0 {
return ""
}
suitableWords := make([]string, 0, n)
scnr := bufio.NewScanner(strings.NewReader(s))
scnr.Split(bufio.ScanWords)
for scnr.Scan() {
word := scnr.Text()
if w := keepWord(word); w != "" {
suitableWords = append(suitableWords, w)
if len(suitableWords) >= n {
break
}
}
}
return strings.Join(suitableWords, " ")
}

78
models/posts_test.go Normal file
View file

@ -0,0 +1,78 @@
package models
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestFirstNWords(t *testing.T) {
tests := []struct {
words int
input string
expected string
}{
{words: 3, input: "This is a test string with multiple words", expected: "This is a"},
{words: 5, input: "Short string", expected: "Short string"},
{words: 0, input: "Empty string", expected: ""},
{words: 3, input: " The rain in Spain etc.", expected: "The rain in"},
{words: 3, input: " The? rain! in$ Spain etc.", expected: "The rain in"},
{words: 3, input: " !!! The 23123 rain ++_+_+ in Spain etc.", expected: "The 23123 rain"},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
result := firstNWords(test.input, test.words, wordForSlug)
assert.Equal(t, test.expected, result)
})
}
}
func TestPost_BestSlug(t *testing.T) {
postDate := time.Date(2023, time.January, 1, 15, 12, 11, 0, time.UTC)
tests := []struct {
name string
post Post
expected string
}{
{
name: "returns slug when slug is set",
post: Post{Slug: "my-custom-slug", Title: "My Title", PublishedAt: postDate},
expected: "my-custom-slug",
},
{
name: "use title when slug is empty",
post: Post{Slug: "", Title: "My Title", PublishedAt: postDate},
expected: "/2023/01/01/my-title",
},
{
name: "use body when slug is empty",
post: Post{Slug: "", Body: "My body", PublishedAt: postDate},
expected: "/2023/01/01/my-body",
},
{
name: "use guid when body is empty",
post: Post{GUID: "abc123", PublishedAt: postDate},
expected: "/2023/01/01/abc123",
},
{
name: "use time component when guid is empty",
post: Post{Slug: "", Title: "", PublishedAt: postDate},
expected: "/2023/01/01/151211",
},
{
name: "use created date if publish date is unset",
post: Post{Slug: "", Title: "a title", CreatedAt: postDate},
expected: "/2023/01/01/a-title",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := test.post.BestSlug()
assert.Equal(t, test.expected, result)
})
}
}