all repos — homestead @ 65039b065ba634b9c4b4c7f4b42ebccdbfd40ce0

Code for my website

domain/content/builder/builder.go (view raw)

package builder

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"slices"
	"time"

	"alin.ovh/gomponents"
	"alin.ovh/x/log"
	"github.com/Southclaws/fault"
	"github.com/Southclaws/fault/fmsg"
	mapset "github.com/deckarep/golang-set/v2"

	"alin.ovh/homestead/domain/content"
	"alin.ovh/homestead/domain/content/builder/template"
	ctemplates "alin.ovh/homestead/domain/content/templates"
	"alin.ovh/homestead/domain/indieweb/sitemap"
	vtemplates "alin.ovh/homestead/domain/vanity/templates"
	"alin.ovh/homestead/domain/web/templates"
	"alin.ovh/homestead/shared/config"
	"alin.ovh/homestead/shared/storage"
	"alin.ovh/homestead/shared/vcs"
)

type Options struct {
	Source       string     `conf:"default:.,short:s,flag:src"`
	Development  bool       `conf:"default:false,flag:dev"`
	VCSRemoteURL config.URL `conf:"default:https://git.alin.ovh/website"`

	Storage storage.Writer  `conf:"-"`
	Repo    *vcs.Repository `conf:"-"`
}

func joinSourcePath(src string) func(string) string {
	return func(rel string) string {
		return filepath.Join(src, rel)
	}
}

func copyFile(storage storage.Writer, src string, rel string) error {
	sf, err := os.Open(src)
	if err != nil {
		return fault.Wrap(err)
	}
	defer sf.Close()
	if err := storage.Write("/"+rel, "", sf); err != nil {
		return fault.Wrap(err)
	}

	return nil
}

func build(
	options *Options,
	config *config.Config,
	log *log.Logger,
) error {
	joinSource := joinSourcePath(options.Source)
	stor := options.Storage
	postDir := "post"
	siteSettings := templates.SiteSettings{
		Title:            config.Title,
		Language:         config.Language,
		Menu:             config.Menu,
		InjectLiveReload: options.Development,
	}

	log.Debug("reading posts", "source", options.Source)
	cc, err := content.NewContentCollection(&content.Config{
		Root:        options.Source,
		PostDir:     postDir,
		Repo:        options.Repo,
		StaticFiles: config.StaticFiles,
	}, log.Named("content"))
	if err != nil {
		return fault.Wrap(err)
	}

	sitemap := sitemap.New(config)
	lastMod := time.Now()
	if len(cc.Posts) > 0 {
		lastMod = cc.Posts[0].Date
	}

	for _, post := range cc.Posts {
		log.Debug("rendering post", "post", post.Basename)
		sitemap.AddPath(post.URL, post.Date)

		if err := stor.WritePost(post, ctemplates.PostPage(siteSettings, post)); err != nil {
			return fault.Wrap(err)
		}
	}

	log.Debug("rendering tags list")
	err = stor.Write("/tags/", "Tags",
		ctemplates.TagsPage(siteSettings, ctemplates.TagsPageVars{
			Title: "Tags",
			Tags:  mapset.Sorted(cc.Tags),
		}),
	)
	if err != nil {
		return fault.Wrap(err)
	}
	sitemap.AddPath("/tags/", lastMod)

	for _, tag := range cc.Tags.ToSlice() {
		matchingPosts := []*content.Post{}
		for _, post := range cc.Posts {
			if slices.Contains(post.Taxonomies.Tags, tag) {
				matchingPosts = append(matchingPosts, post)
			}
		}
		log.Debug("rendering tags page", "tag", tag)
		url := path.Join("/tags", tag) + "/"
		err = stor.Write(url, tag,
			ctemplates.TagPage(siteSettings, ctemplates.TagPageVars{
				Tag:   tag,
				Posts: matchingPosts,
			}),
		)
		if err != nil {
			return fault.Wrap(err)
		}
		sitemap.AddPath(url, matchingPosts[0].Date)

		log.Debug("rendering tags feed", "tag", tag)
		title := fmt.Sprintf("%s - %s", config.Title, tag)
		feed, err := template.RenderFeed(
			title,
			config,
			matchingPosts,
			tag,
		)
		if err != nil {
			return fault.Wrap(err, fmsg.With("could not render tag feed page"))
		}
		file := &storage.File{
			Title:        title,
			LastModified: matchingPosts[0].Date,
			Path:         path.Join("/tags", tag, "atom.xml"),
			FSPath:       path.Join("/tags", tag, "atom.xml"),
		}
		if err := stor.WriteFile(file, feed); err != nil {
			return fault.Wrap(err)
		}
	}

	log.Debug("rendering list page")
	listPage := ctemplates.ListPage(siteSettings, ctemplates.ListPageVars{
		Posts: cc.Posts,
	})
	if err := stor.Write(path.Join("/", postDir)+"/", "Posts", listPage); err != nil {
		return fault.Wrap(err)
	}
	sitemap.AddPath(path.Join("/", postDir)+"/", lastMod)

	log.Debug("rendering feed")
	feed, err := template.RenderFeed(config.Title, config, cc.Posts, "feed")
	if err != nil {
		return fault.Wrap(err, fmsg.With("could not render feed"))
	}
	file := &storage.File{
		Title:        config.Title,
		LastModified: cc.Posts[0].Date,
		Path:         "/atom.xml",
		FSPath:       "/atom.xml",
	}
	if err := stor.WriteFile(file, feed); err != nil {
		return fault.Wrap(err)
	}

	tplFiles, err := template.ListFiles()
	if err != nil {
		return fault.Wrap(err)
	}

	for _, e := range tplFiles {
		filename := e.Name()

		log.Debug("rendering template file", "filename", filename)
		f, err := templates.Files.Open(filename)
		if err != nil {
			return fault.Wrap(err)
		}
		if err := stor.Write("/"+filename, "", templates.MakeWriterTo(f)); err != nil {
			return fault.Wrap(err)
		}
	}

	for _, post := range cc.Pages {
		log.Debug("rendering page", "source", post.Input, "path", post.URL)
		var page gomponents.Node
		if post.URL == "/" {
			page = ctemplates.Homepage(siteSettings, ctemplates.HomepageVars{
				Email: config.PublicEmail,
				Me:    config.RelMe,
				Posts: cc.Posts,
			}, post)
		} else {
			page = ctemplates.Page(siteSettings, post)
		}
		file := stor.NewFileFromPost(post)
		if err := stor.WriteFile(file, page.(gomponents.NodeWriter)); err != nil {
			return fault.Wrap(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
	sitemap.AddPath("/", time.Time{})

	log.Debug("rendering sitemap")
	if err := stor.Write("/sitemap.xml", "sitemap", sitemap); err != nil {
		return fault.Wrap(err)
	}

	log.Debug("rendering robots.txt")
	robots, err := template.RenderRobotsTXT(config.BaseURL)
	if err != nil {
		return fault.Wrap(err)
	}
	if err := stor.Write("/robots.txt", "", robots); err != nil {
		return fault.Wrap(err)
	}

	for _, sf := range cc.StaticFiles {
		src := joinSource(sf)
		log.Debug("copying static file", "sf", sf, "src", src)
		err = copyFile(stor, src, sf)
		if err != nil {
			return fault.Wrap(err)
		}
	}

	log.Debug("rendering go packages")
	for _, p := range config.Go.Packages {
		page := vtemplates.GoPackagePage(siteSettings, &config.Go, p)
		if err := stor.Write(fmt.Sprintf("/go/%s.html", p), siteSettings.Title, page); err != nil {
			return fault.Wrap(err)
		}
	}

	page := vtemplates.GoPackageListPage(siteSettings, &config.Go)
	if err := stor.Write("/go/", siteSettings.Title, page); err != nil {
		return fault.Wrap(err)
	}

	return nil
}

func BuildSite(options *Options, cfg *config.Config, log *log.Logger) error {
	if cfg == nil {
		return fault.New("config is nil")
	}

	return build(options, cfg, log)
}