about summary refs log tree commit diff stats
path: root/internal
diff options
context:
space:
mode:
authorAlan Pearce2025-01-24 22:14:22 +0100
committerAlan Pearce2025-01-24 22:18:34 +0100
commit177955eab572c13b7d99217e54250f377c9a3c9e (patch)
tree49bf935309446867fd3a6c30f1cf35605648e7b0 /internal
parente2b10dee160b5b1a7e06dfb34c137d2a43bccd91 (diff)
downloadwebsite-177955eab572c13b7d99217e54250f377c9a3c9e.tar.lz
website-177955eab572c13b7d99217e54250f377c9a3c9e.tar.zst
website-177955eab572c13b7d99217e54250f377c9a3c9e.zip
abstract builder outputs
Diffstat (limited to 'internal')
-rw-r--r--internal/builder/builder.go120
-rw-r--r--internal/builder/files.go120
-rw-r--r--internal/content/posts.go25
-rw-r--r--internal/outputs/files.go208
4 files changed, 245 insertions, 228 deletions
diff --git a/internal/builder/builder.go b/internal/builder/builder.go
index bb39c43..7c1911e 100644
--- a/internal/builder/builder.go
+++ b/internal/builder/builder.go
@@ -1,10 +1,7 @@
 package builder
 
 import (
-	"context"
 	"fmt"
-	"io"
-	"os"
 	"path"
 	"path/filepath"
 	"slices"
@@ -12,11 +9,11 @@ import (
 
 	"go.alanpearce.eu/website/internal/config"
 	"go.alanpearce.eu/website/internal/content"
+	"go.alanpearce.eu/website/internal/outputs"
 	"go.alanpearce.eu/website/internal/sitemap"
 	"go.alanpearce.eu/website/templates"
 	"go.alanpearce.eu/x/log"
 
-	"github.com/a-h/templ"
 	mapset "github.com/deckarep/golang-set/v2"
 	"gitlab.com/tozd/go/errors"
 )
@@ -31,62 +28,6 @@ type Result struct {
 	Hashes []string
 }
 
-var compressFiles = false
-
-func mkdirp(dirs ...string) error {
-	err := os.MkdirAll(path.Join(dirs...), 0755)
-
-	return errors.WithMessage(err, "could not create directory")
-}
-
-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 := io.Copy(file, output); err != nil {
-		return errors.WithMessage(err, "could not write output file")
-	}
-
-	return nil
-}
-
-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")
-	}
-	defer file.Close()
-
-	if _, err := writer.WriteTo(file); err != nil {
-		return errors.WithMessage(err, "could not write output file")
-	}
-
-	return nil
-}
-
 func joinSourcePath(src string) func(string) string {
 	return func(rel string) string {
 		return filepath.Join(src, rel)
@@ -94,26 +35,27 @@ func joinSourcePath(src string) func(string) string {
 }
 
 func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result, error) {
+	output := outputs.NewFilesOutput(ioConfig.Destination, log, &outputs.Options{
+		CompressFiles: !ioConfig.Development,
+	})
+
 	outDir := ioConfig.Destination
 	joinSource := joinSourcePath(ioConfig.Source)
-	log.Debug("output", "dir", outDir)
+
+	log.Debug("output", "dir", ioConfig.Destination)
 	r := &Result{
 		Hashes: make([]string, 0),
 	}
 
-	err := copyRecursive(joinSource("static"), outDir)
+	err := output.CopyRecursive(joinSource("static"))
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not copy static files")
 	}
 
-	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 := content.ReadPosts(&content.Config{
-		Root:      joinSource("content"),
-		InputDir:  "post",
-		OutputDir: outDir,
+		Root:     joinSource("content"),
+		InputDir: "post",
 	}, log.Named("content"))
 	if err != nil {
 		return nil, err
@@ -126,25 +68,17 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result,
 	}
 
 	for _, post := range posts {
-		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)
 		sitemap.AddPath(post.URL, post.Date)
-		if err := renderToFile(templates.PostPage(config, post), post.Output); err != nil {
+		if err := output.RenderToFile(templates.PostPage(config, post), post.Output); err != nil {
 			return nil, err
 		}
 	}
 
-	if err := mkdirp(outDir, "tags"); err != nil {
-		return nil, errors.WithMessage(err, "could not create directory for tags")
-	}
 	log.Debug("rendering tags list")
-	if err := renderToFile(
+	if err := output.RenderToFile(
 		templates.TagsPage(config, "tags", mapset.Sorted(tags), "/tags"),
-		outDir,
-		"tags",
-		"index.html",
+		path.Join("tags", "index.html"),
 	); err != nil {
 		return nil, err
 	}
@@ -157,17 +91,11 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result,
 				matchingPosts = append(matchingPosts, post)
 			}
 		}
-		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
-		if err := renderToFile(
+		if err := output.RenderToFile(
 			templates.TagPage(config, tag, matchingPosts, url),
-			outDir,
-			"tags",
-			tag,
-			"index.html",
+			path.Join("tags", tag, "index.html"),
 		); err != nil {
 			return nil, err
 		}
@@ -183,13 +111,16 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result,
 		if err != nil {
 			return nil, errors.WithMessage(err, "could not render tag feed page")
 		}
-		if err := writerToFile(feed, outDir, "tags", tag, "atom.xml"); err != nil {
+		if err := output.WriterToFile(feed, path.Join("tags", tag, "atom.xml")); err != nil {
 			return nil, err
 		}
 	}
 
 	log.Debug("rendering list page")
-	if err := renderToFile(templates.ListPage(config, posts, "/post"), outDir, "post", "index.html"); err != nil {
+	if err := output.RenderToFile(
+		templates.ListPage(config, posts, "/post"),
+		path.Join("post", "index.html"),
+	); err != nil {
 		return nil, err
 	}
 	sitemap.AddPath("/post/", lastMod)
@@ -199,7 +130,7 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result,
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not render feed")
 	}
-	if err := writerToFile(feed, outDir, "atom.xml"); err != nil {
+	if err := output.WriterToFile(feed, "atom.xml"); err != nil {
 		return nil, err
 	}
 
@@ -208,7 +139,7 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result,
 	if err != nil {
 		return nil, errors.WithMessage(err, "could not render feed styles")
 	}
-	if err := outputToFile(feedStyles, outDir, "feed-styles.xsl"); err != nil {
+	if err := output.OutputToFile(feedStyles, "feed-styles.xsl"); err != nil {
 		return nil, err
 	}
 	_, err = feedStyles.Seek(0, 0)
@@ -230,7 +161,7 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result,
 	if err != nil {
 		return nil, err
 	}
-	if err := renderToFile(templates.Homepage(config, posts, content), outDir, "index.html"); err != nil {
+	if err := output.RenderToFile(templates.Homepage(config, posts, content), "index.html"); err != nil {
 		return nil, err
 	}
 	// it would be nice to set LastMod here, but using the latest post
@@ -241,7 +172,7 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result,
 	r.Hashes = append(r.Hashes, h)
 
 	log.Debug("rendering sitemap")
-	if err := writerToFile(sitemap, outDir, "sitemap.xml"); err != nil {
+	if err := output.WriterToFile(sitemap, "sitemap.xml"); err != nil {
 		return nil, err
 	}
 
@@ -250,7 +181,7 @@ func build(ioConfig *IOConfig, config *config.Config, log *log.Logger) (*Result,
 	if err != nil {
 		return nil, err
 	}
-	if err := outputToFile(rob, outDir, "robots.txt"); err != nil {
+	if err := output.OutputToFile(rob, "robots.txt"); err != nil {
 		return nil, err
 	}
 
@@ -262,7 +193,6 @@ func BuildSite(ioConfig *IOConfig, cfg *config.Config, log *log.Logger) (*Result
 		return nil, errors.New("config is nil")
 	}
 	cfg.InjectLiveReload = ioConfig.Development
-	compressFiles = !ioConfig.Development
 
 	templates.Setup()
 	loadCSS(ioConfig.Source)
diff --git a/internal/builder/files.go b/internal/builder/files.go
deleted file mode 100644
index a9046d7..0000000
--- a/internal/builder/files.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package builder
-
-import (
-	"compress/gzip"
-	"io"
-	"io/fs"
-	"os"
-	"path/filepath"
-
-	"github.com/andybalholm/brotli"
-)
-
-const (
-	gzipLevel   = 6
-	brotliLevel = 9
-)
-
-type MultiWriteCloser struct {
-	writers     []io.WriteCloser
-	multiWriter io.Writer
-}
-
-func (mw *MultiWriteCloser) Write(p []byte) (n int, err error) {
-	return mw.multiWriter.Write(p)
-}
-
-func (mw *MultiWriteCloser) Close() error {
-	var lastErr error
-	for _, w := range mw.writers {
-		err := w.Close()
-		if err != nil {
-			lastErr = err
-		}
-	}
-
-	return lastErr
-}
-
-func openFileWrite(filename string) (*os.File, error) {
-	return os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
-}
-
-func openFileGz(filename string) (*gzip.Writer, error) {
-	filenameGz := filename + ".gz"
-	f, err := openFileWrite(filenameGz)
-	if err != nil {
-		return nil, err
-	}
-
-	return gzip.NewWriterLevel(f, gzipLevel)
-}
-
-func openFileBrotli(filename string) (*brotli.Writer, error) {
-	filenameBrotli := filename + ".br"
-	f, err := openFileWrite(filenameBrotli)
-	if err != nil {
-		return nil, err
-	}
-
-	return brotli.NewWriterLevel(f, brotliLevel), nil
-}
-
-func multiOpenFile(filename string) (*MultiWriteCloser, error) {
-	r, err := openFileWrite(filename)
-	if err != nil {
-		return nil, err
-	}
-	gz, err := openFileGz(filename)
-	if err != nil {
-		return nil, err
-	}
-	br, err := openFileBrotli(filename)
-	if err != nil {
-		return nil, err
-	}
-
-	return &MultiWriteCloser{
-		writers:     []io.WriteCloser{r, gz, br},
-		multiWriter: io.MultiWriter(r, gz, br),
-	}, nil
-}
-
-func openFileAndVariants(filename string) (io.WriteCloser, error) {
-	if compressFiles {
-		return multiOpenFile(filename)
-	}
-
-	return openFileWrite(filename)
-}
-
-func copyRecursive(src, dst string) error {
-	return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-		rel, err := filepath.Rel(src, path)
-		if err != nil {
-			return err
-		}
-		if d.IsDir() {
-			return mkdirp(dst, rel)
-		}
-
-		sf, err := os.Open(path)
-		if err != nil {
-			return err
-		}
-		defer sf.Close()
-		df, err := openFileAndVariants(filepath.Join(dst, rel))
-		if err != nil {
-			return err
-		}
-		defer df.Close()
-		if _, err := io.Copy(df, sf); err != nil {
-			return err
-		}
-
-		return nil
-	})
-}
diff --git a/internal/content/posts.go b/internal/content/posts.go
index f4c6c76..467f36f 100644
--- a/internal/content/posts.go
+++ b/internal/content/posts.go
@@ -81,9 +81,8 @@ func RenderMarkdown(content []byte) (string, error) {
 }
 
 type Config struct {
-	Root      string
-	InputDir  string
-	OutputDir string
+	Root     string
+	InputDir string
 }
 
 func ReadPosts(config *Config, log *log.Logger) ([]Post, Tags, error) {
@@ -94,15 +93,15 @@ func ReadPosts(config *Config, log *log.Logger) ([]Post, Tags, error) {
 	if err != nil {
 		return nil, nil, errors.WithMessagef(err, "could not read post directory %s", subdir)
 	}
-	outputReplacer := strings.NewReplacer(config.Root, config.OutputDir, ".md", "/index.html")
-	urlReplacer := strings.NewReplacer(config.Root, "", ".md", "/")
+	outputReplacer := strings.NewReplacer(".md", "/index.html")
+	urlReplacer := strings.NewReplacer(".md", "/")
 	for _, f := range files {
-		pathFromRoot := filepath.Join(subdir, f.Name())
-		if !f.IsDir() && path.Ext(pathFromRoot) == ".md" {
-			output := outputReplacer.Replace(pathFromRoot)
-			url := urlReplacer.Replace(pathFromRoot)
-			log.Debug("reading post", "post", pathFromRoot)
-			matter, content, err := GetPost(pathFromRoot)
+		fn := f.Name()
+		if !f.IsDir() && path.Ext(fn) == ".md" {
+			output := path.Join(config.InputDir, outputReplacer.Replace(fn))
+			url := urlReplacer.Replace(strings.TrimPrefix("/", fn))
+			log.Debug("reading post", "post", fn)
+			matter, content, err := GetPost(path.Join(subdir, fn))
 			if err != nil {
 				return nil, nil, err
 			}
@@ -111,13 +110,13 @@ func ReadPosts(config *Config, log *log.Logger) ([]Post, Tags, error) {
 				tags.Add(strings.ToLower(tag))
 			}
 
-			log.Debug("rendering markdown in post", "post", pathFromRoot)
+			log.Debug("rendering markdown in post", "post", fn)
 			html, err := RenderMarkdown(content)
 			if err != nil {
 				return nil, nil, err
 			}
 			post := Post{
-				Input:      pathFromRoot,
+				Input:      fn,
 				Output:     output,
 				Basename:   filepath.Base(url),
 				URL:        url,
diff --git a/internal/outputs/files.go b/internal/outputs/files.go
new file mode 100644
index 0000000..3425d93
--- /dev/null
+++ b/internal/outputs/files.go
@@ -0,0 +1,208 @@
+package outputs
+
+import (
+	"compress/gzip"
+	"context"
+	"io"
+	"io/fs"
+	"os"
+	"path"
+	"path/filepath"
+
+	"go.alanpearce.eu/x/log"
+
+	"github.com/a-h/templ"
+	"github.com/andybalholm/brotli"
+	"gitlab.com/tozd/go/errors"
+)
+
+const (
+	gzipLevel   = 6
+	brotliLevel = 9
+)
+
+type FilesOutput struct {
+	outputDirectory string
+	options         *Options
+	log             *log.Logger
+}
+
+type Options struct {
+	CompressFiles bool
+}
+
+func NewFilesOutput(outputDirectory string, logger *log.Logger, opts *Options) *FilesOutput {
+	return &FilesOutput{
+		outputDirectory: outputDirectory,
+		options:         opts,
+		log:             logger,
+	}
+}
+
+func (f *FilesOutput) CopyRecursive(src string) error {
+	return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		rel, err := filepath.Rel(src, path)
+		if err != nil {
+			return err
+		}
+		if d.IsDir() {
+			return f.mkdirp(rel)
+		}
+
+		sf, err := os.Open(path)
+		if err != nil {
+			return err
+		}
+		defer sf.Close()
+		df, err := f.openFileAndVariants(filepath.Join(f.outputDirectory, rel))
+		if err != nil {
+			return err
+		}
+		defer df.Close()
+		if _, err := io.Copy(df, sf); err != nil {
+			return err
+		}
+
+		return nil
+	})
+}
+
+func (f *FilesOutput) OutputToFile(output io.Reader, filename string) error {
+	fn := path.Join(f.outputDirectory, filename)
+	if err := f.mkdirp(filepath.Dir(filename)); err != nil {
+		return err
+	}
+	f.log.Debug("outputting file", "filename", fn)
+	file, err := f.openFileAndVariants(fn)
+	if err != nil {
+		return errors.WithMessage(err, "could not open output file")
+	}
+	defer file.Close()
+
+	if _, err := io.Copy(file, output); err != nil {
+		return errors.WithMessage(err, "could not write output file")
+	}
+
+	return nil
+}
+
+func (f *FilesOutput) RenderToFile(component templ.Component, filename string) error {
+	fn := path.Join(f.outputDirectory, filename)
+	if err := f.mkdirp(filepath.Dir(filename)); err != nil {
+		return err
+	}
+	f.log.Debug("outputting file", "filename", filename)
+	file, err := f.openFileAndVariants(fn)
+	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 (f *FilesOutput) WriterToFile(writer io.WriterTo, filename string) error {
+	fn := path.Join(f.outputDirectory, filename)
+	if err := f.mkdirp(filepath.Dir(filename)); err != nil {
+		return err
+	}
+	f.log.Debug("outputting file", "filename", fn)
+	file, err := f.openFileAndVariants(fn)
+	if err != nil {
+		return errors.WithMessage(err, "could not open output file")
+	}
+	defer file.Close()
+
+	if _, err := writer.WriteTo(file); err != nil {
+		return errors.WithMessage(err, "could not write output file")
+	}
+
+	return nil
+}
+
+type MultiWriteCloser struct {
+	writers     []io.WriteCloser
+	multiWriter io.Writer
+}
+
+func (mw *MultiWriteCloser) Write(p []byte) (n int, err error) {
+	return mw.multiWriter.Write(p)
+}
+
+func (mw *MultiWriteCloser) Close() error {
+	var lastErr error
+	for _, w := range mw.writers {
+		err := w.Close()
+		if err != nil {
+			lastErr = err
+		}
+	}
+
+	return lastErr
+}
+
+func openFileWrite(filename string) (*os.File, error) {
+	return os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+}
+
+func openFileGz(filename string) (*gzip.Writer, error) {
+	filenameGz := filename + ".gz"
+	f, err := openFileWrite(filenameGz)
+	if err != nil {
+		return nil, err
+	}
+
+	return gzip.NewWriterLevel(f, gzipLevel)
+}
+
+func openFileBrotli(filename string) (*brotli.Writer, error) {
+	filenameBrotli := filename + ".br"
+	f, err := openFileWrite(filenameBrotli)
+	if err != nil {
+		return nil, err
+	}
+
+	return brotli.NewWriterLevel(f, brotliLevel), nil
+}
+
+func multiOpenFile(filename string) (*MultiWriteCloser, error) {
+	r, err := openFileWrite(filename)
+	if err != nil {
+		return nil, err
+	}
+	gz, err := openFileGz(filename)
+	if err != nil {
+		return nil, err
+	}
+	br, err := openFileBrotli(filename)
+	if err != nil {
+		return nil, err
+	}
+
+	return &MultiWriteCloser{
+		writers:     []io.WriteCloser{r, gz, br},
+		multiWriter: io.MultiWriter(r, gz, br),
+	}, nil
+}
+
+func (f *FilesOutput) openFileAndVariants(filename string) (io.WriteCloser, error) {
+	if f.options.CompressFiles {
+		return multiOpenFile(filename)
+	}
+
+	return openFileWrite(filename)
+}
+
+func (f *FilesOutput) mkdirp(dir string) error {
+	f.log.Debug("creating directory", "dir", dir)
+	err := os.MkdirAll(path.Join(f.outputDirectory, dir), 0755)
+
+	return errors.WithMessage(err, "could not create directory")
+}