2026-02-18 11:07:18 +00:00
|
|
|
package sitebuilder
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-03-04 11:33:39 +00:00
|
|
|
"context"
|
2026-02-18 11:07:18 +00:00
|
|
|
"fmt"
|
|
|
|
|
"html/template"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-03-03 11:36:24 +00:00
|
|
|
"golang.org/x/sync/errgroup"
|
2026-02-18 11:07:18 +00:00
|
|
|
"lmika.dev/lmika/weiro/models"
|
2026-02-19 10:21:27 +00:00
|
|
|
"lmika.dev/lmika/weiro/models/pubmodel"
|
2026-03-04 11:33:39 +00:00
|
|
|
"lmika.dev/lmika/weiro/providers/markdown"
|
2026-02-18 11:07:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Builder struct {
|
2026-02-19 10:21:27 +00:00
|
|
|
site pubmodel.Site
|
2026-03-04 11:33:39 +00:00
|
|
|
mdRenderer *markdown.Renderer
|
2026-02-18 11:07:18 +00:00
|
|
|
opts Options
|
|
|
|
|
tmpls *template.Template
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 10:21:27 +00:00
|
|
|
func New(site pubmodel.Site, opts Options) (*Builder, error) {
|
2026-02-18 11:07:18 +00:00
|
|
|
tmpls, err := template.New("").
|
|
|
|
|
Funcs(templateFns(site, opts)).
|
|
|
|
|
ParseFS(opts.TemplatesFS, tmplNamePostSingle, tmplNamePostList, tmplNameLayoutMain)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &Builder{
|
2026-03-04 11:33:39 +00:00
|
|
|
site: site,
|
|
|
|
|
opts: opts,
|
|
|
|
|
tmpls: tmpls,
|
|
|
|
|
mdRenderer: markdown.NewRendererForSite(),
|
2026-02-18 11:07:18 +00:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) BuildSite(outDir string) error {
|
|
|
|
|
buildCtx := buildContext{outDir: outDir}
|
|
|
|
|
|
|
|
|
|
if err := os.RemoveAll(outDir); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:36:24 +00:00
|
|
|
eg := errgroup.Group{}
|
|
|
|
|
|
|
|
|
|
eg.Go(func() error {
|
|
|
|
|
for _, post := range b.site.Posts {
|
|
|
|
|
if err := b.writePost(buildCtx, post); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
eg.Go(func() error {
|
|
|
|
|
if err := b.renderPostList(buildCtx, b.site.Posts); err != nil {
|
2026-02-18 11:07:18 +00:00
|
|
|
return err
|
|
|
|
|
}
|
2026-03-03 11:36:24 +00:00
|
|
|
return nil
|
|
|
|
|
})
|
2026-02-18 11:07:18 +00:00
|
|
|
|
2026-03-03 11:36:24 +00:00
|
|
|
// Copy uploads
|
|
|
|
|
eg.Go(func() error {
|
|
|
|
|
if err := b.writeUploads(buildCtx, b.site.Uploads); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err := eg.Wait(); err != nil {
|
2026-02-18 11:07:18 +00:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) renderPostList(ctx buildContext, postList []*models.Post) error {
|
|
|
|
|
// TODO: paging
|
|
|
|
|
postCopy := make([]*models.Post, len(postList))
|
|
|
|
|
copy(postCopy, postList)
|
|
|
|
|
|
|
|
|
|
sort.Slice(postCopy, func(i, j int) bool {
|
2026-02-19 10:21:27 +00:00
|
|
|
return postCopy[i].PublishedAt.After(postCopy[j].PublishedAt)
|
2026-02-18 11:07:18 +00:00
|
|
|
})
|
|
|
|
|
|
2026-02-18 11:38:05 +00:00
|
|
|
pl := postListData{
|
2026-02-19 10:21:27 +00:00
|
|
|
commonData: commonData{Site: b.site},
|
2026-02-18 11:38:05 +00:00
|
|
|
}
|
2026-02-18 11:07:18 +00:00
|
|
|
for _, post := range postCopy {
|
|
|
|
|
rp, err := b.renderPost(post)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
pl.Posts = append(pl.Posts, rp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.createAtPath(ctx, "", func(f io.Writer) error {
|
|
|
|
|
return b.renderTemplate(f, tmplNamePostList, pl)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) renderPost(post *models.Post) (postSingleData, error) {
|
2026-02-19 10:21:27 +00:00
|
|
|
postPath := post.Slug
|
2026-02-18 11:07:18 +00:00
|
|
|
if b.opts.BasePosts != "" {
|
|
|
|
|
postPath = filepath.Join(b.opts.BasePosts, strings.TrimPrefix(postPath, "/"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var md bytes.Buffer
|
2026-03-04 11:33:39 +00:00
|
|
|
if err := b.mdRenderer.RenderTo(context.Background(), &md, post.Body); err != nil {
|
2026-02-19 10:21:27 +00:00
|
|
|
return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Slug, err)
|
2026-02-18 11:07:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return postSingleData{
|
2026-02-19 10:21:27 +00:00
|
|
|
commonData: commonData{Site: b.site},
|
2026-02-18 11:38:05 +00:00
|
|
|
Path: postPath,
|
2026-02-19 11:29:44 +00:00
|
|
|
Post: post,
|
2026-02-18 11:38:05 +00:00
|
|
|
HTML: template.HTML(md.String()),
|
2026-02-18 11:07:18 +00:00
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) writePost(ctx buildContext, post *models.Post) error {
|
|
|
|
|
rp, err := b.renderPost(post)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.createAtPath(ctx, rp.Path, func(f io.Writer) error {
|
|
|
|
|
return b.renderTemplate(f, tmplNamePostSingle, rp)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) createAtPath(ctx buildContext, path string, fn func(f io.Writer) error) error {
|
|
|
|
|
outFile := filepath.Join(ctx.outDir, strings.TrimPrefix(path, "/"))
|
|
|
|
|
if filepath.Ext(outFile) == "" {
|
|
|
|
|
outFile = filepath.Join(outFile, "index.html")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render it within the template
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(outFile), 0755); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f, err := os.Create(outFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
|
|
return fn(f)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (b *Builder) renderTemplate(w io.Writer, name string, data interface{}) error {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
if err := b.tmpls.ExecuteTemplate(&buf, name, data); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.tmpls.ExecuteTemplate(w, tmplNameLayoutMain, layoutData{
|
2026-02-19 10:21:27 +00:00
|
|
|
commonData: commonData{Site: b.site},
|
2026-02-18 11:38:05 +00:00
|
|
|
Body: template.HTML(buf.String()),
|
2026-02-18 11:07:18 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 11:36:24 +00:00
|
|
|
func (b *Builder) writeUploads(ctx buildContext, uploads []models.Upload) error {
|
|
|
|
|
for _, u := range uploads {
|
|
|
|
|
fullPath := filepath.Join(ctx.outDir, "uploads", u.Slug)
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := func() error {
|
|
|
|
|
r, err := b.site.OpenUpload(u)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer r.Close()
|
|
|
|
|
|
|
|
|
|
w, err := os.Create(fullPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer w.Close()
|
|
|
|
|
|
|
|
|
|
if _, err := io.Copy(w, r); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 11:07:18 +00:00
|
|
|
type buildContext struct {
|
|
|
|
|
outDir string
|
|
|
|
|
}
|