about summary refs log tree commit diff stats
path: root/internal/builder/builder.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/builder/builder.go')
-rw-r--r--internal/builder/builder.go277
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)
 }