diff options
Diffstat (limited to 'internal/builder/builder.go')
-rw-r--r-- | internal/builder/builder.go | 277 |
1 files changed, 141 insertions, 136 deletions
diff --git a/internal/builder/builder.go b/internal/builder/builder.go index b17fbc2..b99d919 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -1,54 +1,80 @@ package builder import ( + "context" "fmt" "io" - "net/url" "os" "path" + "path/filepath" "slices" - "sync" "time" - "website/internal/config" - "website/internal/log" + "go.alanpearce.eu/website/internal/config" + "go.alanpearce.eu/website/internal/content" + "go.alanpearce.eu/x/log" + "go.alanpearce.eu/website/internal/sitemap" + "go.alanpearce.eu/website/templates" - cp "github.com/otiai10/copy" - "github.com/pkg/errors" - "github.com/snabb/sitemap" + "github.com/a-h/templ" + mapset "github.com/deckarep/golang-set/v2" + "gitlab.com/tozd/go/errors" ) type IOConfig struct { Source string `conf:"default:.,short:s,flag:src"` - Destination string `conf:"default:website,short:d,flag:dest"` - BaseURL config.URL - Development bool `conf:"default:false,flag:dev"` + Destination string `conf:"default:public,short:d,flag:dest"` + Development bool `conf:"default:false,flag:dev"` } +type Result struct { + Hashes []string +} + +var compressFiles = false + func mkdirp(dirs ...string) error { err := os.MkdirAll(path.Join(dirs...), 0755) return errors.Wrap(err, "could not create directory") } -func outputToFile(output io.Reader, filename ...string) error { - log.Debug("outputting file", "filename", path.Join(filename...)) - file, err := os.OpenFile(path.Join(filename...), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) +func outputToFile(output io.Reader, pathParts ...string) error { + filename := path.Join(pathParts...) + // log.Debug("outputting file", "filename", filename) + file, err := openFileAndVariants(filename) if err != nil { return errors.WithMessage(err, "could not open output file") } defer file.Close() - if _, err := file.ReadFrom(output); err != nil { + if _, err := io.Copy(file, output); err != nil { return errors.WithMessage(err, "could not write output file") } return nil } -func writerToFile(writer io.WriterTo, filename ...string) error { - log.Debug("outputting file", "filename", path.Join(filename...)) - file, err := os.OpenFile(path.Join(filename...), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) +func renderToFile(component templ.Component, pathParts ...string) error { + filename := path.Join(pathParts...) + // log.Debug("outputting file", "filename", filename) + file, err := openFileAndVariants(filename) + if err != nil { + return errors.WithMessage(err, "could not open output file") + } + defer file.Close() + + if err := component.Render(context.TODO(), file); err != nil { + return errors.WithMessage(err, "could not write output file") + } + + return nil +} + +func writerToFile(writer io.WriterTo, pathParts ...string) error { + filename := path.Join(pathParts...) + // log.Debug("outputting file", "filename", path.Join(filename...)) + file, err := openFileAndVariants(filename) if err != nil { return errors.WithMessage(err, "could not open output file") } @@ -61,206 +87,185 @@ func writerToFile(writer io.WriterTo, filename ...string) error { return nil } -func build(outDir string, config config.Config) error { - log.Debug("output", "dir", outDir) - assetsOnce = sync.Once{} - privateDir := path.Join(outDir, "private") - if err := mkdirp(privateDir); err != nil { - return errors.WithMessage(err, "could not create private directory") +func joinSourcePath(src string) func(string) string { + return func(rel string) string { + return filepath.Join(src, rel) } - publicDir := path.Join(outDir, "public") - if err := mkdirp(publicDir); err != nil { - return errors.WithMessage(err, "could not create public directory") +} + +func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result, error) { + outDir := ioConfig.Destination + joinSource := joinSourcePath(ioConfig.Source) + log.Debug("output", "dir", outDir) + r := &Result{ + Hashes: make([]string, 0), } - err := cp.Copy("static", publicDir, cp.Options{ - PreserveTimes: true, - PermissionControl: cp.AddPermission(0755), - }) + err := copyRecursive(joinSource("static"), outDir) if err != nil { - return errors.WithMessage(err, "could not copy static files") + return nil, errors.WithMessage(err, "could not copy static files") } - if err := mkdirp(publicDir, "post"); err != nil { - return errors.WithMessage(err, "could not create post output directory") + if err := mkdirp(outDir, "post"); err != nil { + return nil, errors.WithMessage(err, "could not create post output directory") } log.Debug("reading posts") - posts, tags, err := readPosts("content", "post", publicDir) + posts, tags, err := content.ReadPosts(&content.Config{ + Root: joinSource("content"), + InputDir: "post", + OutputDir: outDir, + }, log.Named("content")) if err != nil { - return err + return nil, err } - sm := NewSitemap(config) + sitemap := sitemap.New(config) lastMod := time.Now() if len(posts) > 0 { lastMod = posts[0].Date } for _, post := range posts { - if err := mkdirp(publicDir, "post", post.Basename); err != nil { - return errors.WithMessage(err, "could not create directory for post") + if err := mkdirp(outDir, "post", post.Basename); err != nil { + return nil, errors.WithMessage(err, "could not create directory for post") } log.Debug("rendering post", "post", post.Basename) - sm.Add(&sitemap.URL{ - Loc: post.URL, - LastMod: &post.Date, - }) - output, err := renderPost(post, config) - if err != nil { - return errors.WithMessagef(err, "could not render post %s", post.Input) - } - if err := outputToFile(output, post.Output); err != nil { - return err + sitemap.AddPath(post.URL, post.Date) + if err := renderToFile(templates.PostPage(config, post), post.Output); err != nil { + return nil, err } } - if err := mkdirp(publicDir, "tags"); err != nil { - return errors.WithMessage(err, "could not create directory for tags") + if err := mkdirp(outDir, "tags"); err != nil { + return nil, errors.WithMessage(err, "could not create directory for tags") } log.Debug("rendering tags list") - output, err := renderTags(tags, config, "/tags") - if err != nil { - return errors.WithMessage(err, "could not render tags") + if err := renderToFile( + templates.TagsPage(config, "tags", mapset.Sorted(tags), "/tags"), + outDir, + "tags", + "index.html", + ); err != nil { + return nil, err } - if err := outputToFile(output, publicDir, "tags", "index.html"); err != nil { - return err - } - sm.Add(&sitemap.URL{ - Loc: "/tags/", - LastMod: &lastMod, - }) + sitemap.AddPath("/tags/", lastMod) for _, tag := range tags.ToSlice() { - matchingPosts := []Post{} + matchingPosts := []content.Post{} for _, post := range posts { if slices.Contains(post.Taxonomies.Tags, tag) { matchingPosts = append(matchingPosts, post) } } - if err := mkdirp(publicDir, "tags", tag); err != nil { - return errors.WithMessage(err, "could not create directory") + if err := mkdirp(outDir, "tags", tag); err != nil { + return nil, errors.WithMessage(err, "could not create directory") } log.Debug("rendering tags page", "tag", tag) url := "/tags/" + tag - output, err := renderListPage(tag, config, matchingPosts, url) - if err != nil { - return errors.WithMessage(err, "could not render tag page") - } - if err := outputToFile(output, publicDir, "tags", tag, "index.html"); err != nil { - return err + if err := renderToFile( + templates.TagPage(config, tag, matchingPosts, url), + outDir, + "tags", + tag, + "index.html", + ); err != nil { + return nil, err } - sm.Add(&sitemap.URL{ - Loc: url, - LastMod: &matchingPosts[0].Date, - }) + sitemap.AddPath(url, matchingPosts[0].Date) log.Debug("rendering tags feed", "tag", tag) - output, err = renderFeed( + feed, err := renderFeed( fmt.Sprintf("%s - %s", config.Title, tag), config, matchingPosts, tag, ) if err != nil { - return errors.WithMessage(err, "could not render tag feed page") + return nil, errors.WithMessage(err, "could not render tag feed page") } - if err := outputToFile(output, publicDir, "tags", tag, "atom.xml"); err != nil { - return err + if err := writerToFile(feed, outDir, "tags", tag, "atom.xml"); err != nil { + return nil, err } } log.Debug("rendering list page") - listPage, err := renderListPage("", config, posts, "/post") - if err != nil { - return errors.WithMessage(err, "could not render list page") + if err := renderToFile(templates.ListPage(config, posts, "/post"), outDir, "post", "index.html"); err != nil { + return nil, err } - if err := outputToFile(listPage, publicDir, "post", "index.html"); err != nil { - return err - } - sm.Add(&sitemap.URL{ - Loc: "/post/", - LastMod: &lastMod, - }) + sitemap.AddPath("/post/", lastMod) log.Debug("rendering feed") feed, err := renderFeed(config.Title, config, posts, "feed") if err != nil { - return errors.WithMessage(err, "could not render feed") + return nil, errors.WithMessage(err, "could not render feed") } - if err := outputToFile(feed, publicDir, "atom.xml"); err != nil { - return err + if err := writerToFile(feed, outDir, "atom.xml"); err != nil { + return nil, err } log.Debug("rendering feed styles") - feedStyles, err := renderFeedStyles() + feedStyles, err := renderFeedStyles(ioConfig.Source) + if err != nil { + return nil, errors.WithMessage(err, "could not render feed styles") + } + if err := outputToFile(feedStyles, outDir, "feed-styles.xsl"); err != nil { + return nil, err + } + _, err = feedStyles.Seek(0, 0) if err != nil { - return errors.WithMessage(err, "could not render feed styles") + return nil, err } - if err := outputToFile(feedStyles, publicDir, "feed-styles.xsl"); err != nil { - return err + h, err := getFeedStylesHash(feedStyles) + if err != nil { + return nil, err } + r.Hashes = append(r.Hashes, h) log.Debug("rendering homepage") - homePage, err := renderHomepage(config, posts, "/") + _, text, err := content.GetPost(joinSource(filepath.Join("content", "index.md"))) + if err != nil { + return nil, err + } + content, err := content.RenderMarkdown(text) if err != nil { - return errors.WithMessage(err, "could not render homepage") + return nil, err } - if err := outputToFile(homePage, publicDir, "index.html"); err != nil { - return err + if err := renderToFile(templates.Homepage(config, posts, content), outDir, "index.html"); err != nil { + return nil, err } // it would be nice to set LastMod here, but using the latest post // date would be wrong as the homepage has its own content file // without a date, which could be newer - sm.Add(&sitemap.URL{ - Loc: "/", - }) - - log.Debug("rendering 404 page") - notFound, err := render404(config, "/404.html") - if err != nil { - return errors.WithMessage(err, "could not render 404 page") - } - if err := outputToFile(notFound, publicDir, "404.html"); err != nil { - return err - } + sitemap.AddPath("/", time.Time{}) + h, _ = getHTMLStyleHash(outDir, "index.html") + r.Hashes = append(r.Hashes, h) log.Debug("rendering sitemap") - if err := writerToFile(sm, publicDir, "sitemap.xml"); err != nil { - return err + if err := writerToFile(sitemap, outDir, "sitemap.xml"); err != nil { + return nil, err } log.Debug("rendering robots.txt") - rob, err := renderRobotsTXT(config) + rob, err := renderRobotsTXT(ioConfig.Source, config) if err != nil { - return err + return nil, err } - if err := outputToFile(rob, publicDir, "robots.txt"); err != nil { - return err + if err := outputToFile(rob, outDir, "robots.txt"); err != nil { + return nil, err } - return nil + return r, nil } -func BuildSite(ioConfig IOConfig) error { - config, err := config.GetConfig() - if err != nil { - return errors.WithMessage(err, "could not get config") +func BuildSite(ioConfig *IOConfig, cfg *config.Config, log *log.Logger) (*Result, error) { + if cfg == nil { + return nil, errors.New("config is nil") } - config.InjectLiveReload = ioConfig.Development + cfg.InjectLiveReload = ioConfig.Development + compressFiles = !ioConfig.Development - if ioConfig.BaseURL.URL != nil { - config.BaseURL.URL, err = url.Parse(ioConfig.BaseURL.String()) - if err != nil { - return errors.WithMessage(err, "could not re-parse base URL") - } - } - - if ioConfig.Development && ioConfig.Destination != "website" { - err = os.RemoveAll(ioConfig.Destination) - if err != nil { - return errors.WithMessage(err, "could not remove destination directory") - } - } + templates.Setup() + loadCSS(ioConfig.Source) - return build(ioConfig.Destination, *config) + return build(ioConfig, cfg, log) } |