Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions metal/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,27 @@ func main() {

return
case 4:
if err = generateSEO(); err != nil {
if err = generateStaticSEO(); err != nil {
cli.Errorln(err.Error())
continue
}

return
case 5:
if err = generatePostsSEO(); err != nil {
cli.Errorln(err.Error())
continue
}

return
case 6:
if err = generatePostSEOForSlug(menu); err != nil {
cli.Errorln(err.Error())
continue
}

return
case 7:
if err = printTimestamp(); err != nil {
cli.Errorln(err.Error())
continue
Expand Down Expand Up @@ -150,22 +164,46 @@ func showApiAccount(menu panel.Menu) error {
return nil
}

func generateSEO() error {
func runSEOGeneration(genFunc func(*seo.Generator) error) error {
gen, err := newSEOGenerator()
if err != nil {
return err
}

return genFunc(gen)
}

func generateStaticSEO() error {
return runSEOGeneration((*seo.Generator).GenerateStaticPages)
}

func generatePostsSEO() error {
return runSEOGeneration((*seo.Generator).GeneratePosts)
}

func generatePostSEOForSlug(menu panel.Menu) error {
slug, err := menu.CapturePostSlug()
if err != nil {
return err
}

return runSEOGeneration(func(gen *seo.Generator) error {
return gen.GeneratePost(slug)
})
}

func newSEOGenerator() (*seo.Generator, error) {
gen, err := seo.NewGenerator(
dbConn,
environment,
portal.GetDefaultValidator(),
)

if err != nil {
return err
return nil, err
}

if err = gen.Generate(); err != nil {
return err
}

return nil
return gen, nil
}

func printTimestamp() error {
Expand Down
29 changes: 27 additions & 2 deletions metal/cli/panel/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/url"
"os"
"regexp"
"strconv"
"strings"

Expand All @@ -21,6 +22,8 @@ type Menu struct {
Validator *portal.Validator
}

var slugPattern = regexp.MustCompile(`^[a-z0-9-]+$`)

func MakeMenu() Menu {
menu := Menu{
Reader: bufio.NewReader(os.Stdin),
Expand Down Expand Up @@ -89,8 +92,10 @@ func (p *Menu) Print() {
p.PrintOption("1) Parse Blog Posts.", inner)
p.PrintOption("2) Create new API account.", inner)
p.PrintOption("3) Show API accounts.", inner)
p.PrintOption("4) Generate SEO.", inner)
p.PrintOption("5) Print Timestamp.", inner)
p.PrintOption("4) Generate SEO for static pages.", inner)
p.PrintOption("5) Generate SEO for blog posts.", inner)
p.PrintOption("6) Generate SEO for a blog post by slug.", inner)
p.PrintOption("7) Print Timestamp.", inner)

p.PrintOption(" ", inner)
p.PrintOption("0) Exit.", inner)
Expand Down Expand Up @@ -183,3 +188,23 @@ func (p *Menu) CapturePostURL() (*posts.Input, error) {

return &input, nil
}

func (p *Menu) CapturePostSlug() (string, error) {
fmt.Print("Enter the blog post slug: ")

slug, err := p.Reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("%sError reading the post slug: %v %s", cli.RedColour, err, cli.Reset)
}

slug = strings.TrimSpace(slug)
if slug == "" {
return "", fmt.Errorf("%sError: no slug provided: %s", cli.RedColour, cli.Reset)
}

if !slugPattern.MatchString(slug) {
return "", fmt.Errorf("%sError: slug must contain only lowercase letters, numbers, or hyphens: %s", cli.RedColour, cli.Reset)
}

return slug, nil
}
20 changes: 20 additions & 0 deletions metal/cli/panel/menu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,23 @@ func TestCapturePostURL(t *testing.T) {
t.Fatalf("expected error")
}
}

func TestCapturePostSlug(t *testing.T) {
m := Menu{
Reader: bufio.NewReader(strings.NewReader("valid-slug\n")),
}

slug, err := m.CapturePostSlug()

if err != nil || slug != "valid-slug" {
t.Fatalf("got %q err %v", slug, err)
}

bad := Menu{
Reader: bufio.NewReader(strings.NewReader("Invalid Slug\n")),
}

if _, err := bad.CapturePostSlug(); err == nil {
t.Fatalf("expected error")
}
}
91 changes: 69 additions & 22 deletions metal/cli/seo/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/oullin/metal/router"
"github.com/oullin/pkg/cli"
"github.com/oullin/pkg/portal"
"gorm.io/gorm"
)

//go:embed stub.html
Expand Down Expand Up @@ -93,9 +94,9 @@ func NewGenerator(db *database.Connection, env *env.Environment, val *portal.Val
}, nil
}

func (g *Generator) Generate() error {
cli.Magentaln("Starting SEO generation pipeline")
defer cli.Magentaln("SEO generation pipeline finished")
func (g *Generator) GenerateStaticPages() error {
cli.Magentaln("Starting static SEO generation pipeline")
defer cli.Magentaln("Static SEO generation pipeline finished")

steps := []struct {
name string
Expand All @@ -105,7 +106,6 @@ func (g *Generator) Generate() error {
{"about", g.GenerateAbout},
{"projects", g.GenerateProjects},
{"resume", g.GenerateResume},
{"posts", g.GeneratePosts},
}

for _, step := range steps {
Expand Down Expand Up @@ -150,7 +150,11 @@ func (g *Generator) GenerateIndex() error {
// ----- Template Parsing

web := g.Web.GetHomePage()
tData, buildErr := g.buildForPage(web.Name, web.Url, html)
tData, buildErr := g.buildForPage(web.Name, web.Url, html, func(data *TemplateData) {
data.Title = web.Title
data.Description = web.Excerpt
})

if buildErr != nil {
return fmt.Errorf("home: generating template data: %w", buildErr)
}
Expand Down Expand Up @@ -192,7 +196,7 @@ func (g *Generator) GenerateAbout() error {

web := g.Web.GetAboutPage()
data, buildErr := g.buildForPage(web.Name, web.Url, html, func(data *TemplateData) {
data.Title = g.TitleFor(web.Title)
data.Title = web.Title
data.Description = web.Excerpt
})

Expand Down Expand Up @@ -222,7 +226,7 @@ func (g *Generator) GenerateProjects() error {

web := g.Web.GetProjectsPage()
data, buildErr := g.buildForPage(web.Name, web.Url, body, func(data *TemplateData) {
data.Title = g.TitleFor(web.Title)
data.Title = web.Title
data.Description = web.Excerpt
})

Expand Down Expand Up @@ -268,7 +272,7 @@ func (g *Generator) GenerateResume() error {

web := g.Web.GetResumePage()
data, buildErr := g.buildForPage(web.Name, web.Url, html, func(data *TemplateData) {
data.Title = g.TitleFor(web.Title)
data.Title = web.Title
data.Description = web.Excerpt
})

Expand All @@ -286,6 +290,9 @@ func (g *Generator) GenerateResume() error {
}

func (g *Generator) GeneratePosts() error {
cli.Magentaln("Starting blog posts SEO generation pipeline")
defer cli.Magentaln("Blog posts SEO generation pipeline finished")

var posts []database.Post

err := g.DB.Sql().
Expand All @@ -312,25 +319,65 @@ func (g *Generator) GeneratePosts() error {
sections := NewSections()

for _, post := range posts {
cli.Cyanln(fmt.Sprintf("Building SEO for post: %s", post.Slug))
response := payload.GetPostsResponse(post)
cli.Grayln(fmt.Sprintf("Post slug: %s", response.Slug))
cli.Grayln(fmt.Sprintf("Post title: %s", response.Title))
body := []template.HTML{sections.Post(&response)}

data, buildErr := g.BuildForPost(response, body)
if buildErr != nil {
return fmt.Errorf("posts: building seo for %s: %w", response.Slug, buildErr)
if err := g.generatePostSEO(sections, post); err != nil {
return fmt.Errorf("posts: %w", err)
}
}

origin := filepath.Join("posts", response.Slug)
if err = g.Export(origin, data); err != nil {
return fmt.Errorf("posts: exporting %s: %w", response.Slug, err)
}
return nil
}

func (g *Generator) GeneratePost(slug string) error {
cli.Magentaln(fmt.Sprintf("Starting blog post SEO generation pipeline for %s", slug))
defer cli.Magentaln(fmt.Sprintf("Blog post SEO generation pipeline finished for %s", slug))

var post database.Post

err := g.DB.Sql().
Model(&database.Post{}).
Preload("Author").
Preload("Categories").
Preload("Tags").
Where("posts.slug = ?", slug).
Where("posts.published_at IS NOT NULL").
Where("posts.deleted_at IS NULL").
First(&post).Error

cli.Successln(fmt.Sprintf("Post SEO template generated for %s", response.Slug))
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("post %s: not found or not published", slug)
}

if err != nil {
return fmt.Errorf("post %s: fetching post: %w", slug, err)
}

if err := g.generatePostSEO(NewSections(), post); err != nil {
return fmt.Errorf("post %s: %w", slug, err)
}

return nil
}

func (g *Generator) generatePostSEO(sections Sections, post database.Post) error {
cli.Cyanln(fmt.Sprintf("Building SEO for post: %s", post.Slug))

response := payload.GetPostsResponse(post)
cli.Grayln(fmt.Sprintf("Post slug: %s", response.Slug))
cli.Grayln(fmt.Sprintf("Post title: %s", response.Title))
body := []template.HTML{sections.Post(&response)}

data, buildErr := g.BuildForPost(response, body)
if buildErr != nil {
return fmt.Errorf("building seo for %s: %w", response.Slug, buildErr)
}

origin := filepath.Join("posts", response.Slug)
if err := g.Export(origin, data); err != nil {
return fmt.Errorf("exporting %s: %w", response.Slug, err)
}

cli.Successln(fmt.Sprintf("Post SEO template generated for %s", response.Slug))

return nil
}

Expand Down
12 changes: 10 additions & 2 deletions metal/cli/seo/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,16 @@ func TestGeneratorGenerateAllPages(t *testing.T) {
t.Fatalf("expected categories from database")
}

if err := gen.Generate(); err != nil {
t.Fatalf("generate err: %v", err)
if err := gen.GenerateStaticPages(); err != nil {
t.Fatalf("generate static err: %v", err)
}

if err := gen.GeneratePosts(); err != nil {
t.Fatalf("generate posts err: %v", err)
}

if err := gen.GeneratePost(post.Slug); err != nil {
t.Fatalf("generate post by slug err: %v", err)
}

output := filepath.Join(env.Seo.SpaDir, "index.seo.html")
Expand Down
2 changes: 1 addition & 1 deletion metal/cli/seo/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func NewWeb() *Web {
about := WebPage{
Name: "About",
Url: "/about",
Title: "About " + AuthorName,
Title: "Who is " + AuthorName,
Excerpt: "Gus's an engineering leader who’s passionate about building reliable and smooth software that strive to make a difference. He also has led teams in designing and delivering scalable, high-performance systems that run efficiently even in complex environments",
}

Expand Down
Loading