2024-07-30 00:12:12 +02:00

165 lines
3.5 KiB
Go

package main
import (
"bytes"
"fmt"
"os"
"slices"
"sort"
"strings"
"git.akyoto.dev/go/markdown"
"git.akyoto.dev/go/web"
"git.akyoto.dev/go/web/send"
)
type App struct {
html string
posts map[string]*Post
projects []string
}
func New() *App {
html := loadClean("public/app.html")
css := loadClean("public/app.css")
html = strings.Replace(html, "{head}", fmt.Sprintf("{head}<style>%s</style>", css), 1)
html = strings.ReplaceAll(html, "{avatar}", "https://gravatar.com/avatar/35f2c481f711f0a36bc0e930c4c15eb0bcc794aaeef405a060fe3e28d1c7b7e5.png?s=64")
posts := loadPosts("posts")
projects := loadProjects("projects")
return &App{
html: html,
posts: posts,
projects: projects,
}
}
func (app *App) Run() {
s := web.NewServer()
s.Use(func(ctx web.Context) error {
defer func() {
err := recover()
if err != nil {
fmt.Println("Recovered panic:", err)
}
}()
return ctx.Next()
})
s.Use(func(ctx web.Context) error {
path := ctx.Request().Path()
if len(path) > 1 && strings.HasSuffix(path, "/") {
return ctx.Redirect(301, strings.TrimSuffix(path, "/"))
}
return ctx.Next()
})
render := func(ctx web.Context, head string, body string) error {
html := app.html
html = strings.Replace(html, "{head}", head, 1)
html = strings.Replace(html, "{body}", body, 1)
return send.HTML(ctx, html)
}
renderPost := func(ctx web.Context, id string) error {
post := app.posts[id]
head := fmt.Sprintf(`<title>%s</title><meta name="keywords" content="%s">`, post.Title, strings.Join(post.Tags, ","))
content := ""
if slices.Contains(post.Tags, "article") {
content = fmt.Sprintf(
`<article><header><h1>%s</h1><time datetime="%s">%s</time></header>%s</article>`,
post.Title,
post.Created,
post.Created[:len("YYYY-MM-DD")],
markdown.Render(post.Content),
)
} else {
content = fmt.Sprintf(
`<h2>%s</h2>%s`,
post.Title,
markdown.Render(post.Content),
)
}
return render(ctx, head, content)
}
s.Get("/", func(ctx web.Context) error {
head := fmt.Sprintf(`<title>%s</title><meta name="keywords" content="%s">`, "Projects", "projects")
body := strings.Builder{}
body.WriteString(`<h2>Projects</h2>`)
for i, markdown := range app.projects {
body.WriteString(markdown)
if i != len(app.projects)-1 {
body.WriteString(`<hr>`)
}
}
return render(ctx, head, body.String())
})
s.Get("/blog", func(ctx web.Context) error {
html := bytes.Buffer{}
html.WriteString(`<h2>Blog</h2><ul class="blog">`)
articles := []*Post{}
for _, post := range app.posts {
if !post.Published || !slices.Contains(post.Tags, "article") {
continue
}
articles = append(articles, post)
}
sort.Slice(articles, func(i, j int) bool {
return articles[i].Created > articles[j].Created
})
for _, post := range articles {
fmt.Fprintf(&html, `<li><a href="/%s">%s</a><time datetime="%s">%s</time></li>`, post.Slug, post.Title, post.Created, post.Created[:len("YYYY")])
}
html.WriteString(`</ul>`)
return render(ctx, "<title>akyoto.dev</title>", html.String())
})
s.Get("/:post", func(ctx web.Context) error {
id := ctx.Request().Param("post")
return renderPost(ctx, id)
})
address := os.Getenv("LISTEN")
if address == "" {
address = ":8080"
}
s.Run(address)
}
func load(path string) string {
dataBytes, err := os.ReadFile(path)
if err != nil {
panic(err)
}
return string(dataBytes)
}
func loadClean(path string) string {
data := load(path)
data = strings.ReplaceAll(data, "\t", "")
data = strings.ReplaceAll(data, "\n", "")
return data
}