From a4279aae0d7f0ee6917faa2ae47687be9714a285 Mon Sep 17 00:00:00 2001 From: Facundo Olano Date: Fri, 16 Feb 2024 12:39:19 -0300 Subject: [PATCH] Introduce config mod and struct (#2) * outline config mod and struct * replace site interfaces * load config from config.yml * adapt commands to load and pass config * fix test * fix tests * use symlinks for static assets * implement absolute url filter * doc comment * fix tests * remove outdated TODO comments * Add go build actions workflow (#3) * Add go build actions workflow * set go to 1.22 --- commands/commands.go | 22 +++---- commands/serve.go | 36 ++++++----- config/config.go | 116 ++++++++++++++++++++++++++++++++++++ go.mod | 9 ++- go.sum | 9 +-- main.go | 6 +- site/site.go | 87 ++++++++++++++------------- site/site_test.go | 77 ++++++++++++------------ templates/filters.go | 16 +++-- templates/templates.go | 6 +- templates/templates_test.go | 16 ++--- 11 files changed, 265 insertions(+), 135 deletions(-) create mode 100644 config/config.go diff --git a/commands/commands.go b/commands/commands.go index 161a872..6613d97 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -2,17 +2,11 @@ package commands import ( "fmt" - "path/filepath" + "github.com/facundoolano/blorg/config" "github.com/facundoolano/blorg/site" ) -const SRC_DIR = "src" -const TARGET_DIR = "target" -const LAYOUTS_DIR = "layouts" -const INCLUDES_DIR = "includes" -const DATA_DIR = "data" - func Init() error { // get working directory // default to . @@ -34,15 +28,15 @@ func New() error { // Read the files in src/ render them and copy the result to target/ func Build(root string) error { - src := filepath.Join(root, SRC_DIR) - target := filepath.Join(root, TARGET_DIR) - layouts := filepath.Join(root, LAYOUTS_DIR) - data := filepath.Join(root, DATA_DIR) - - site, err := site.Load(src, layouts, data) + config, err := config.Load(root) if err != nil { return err } - return site.Build(src, target, true, false) + site, err := site.Load(*config) + if err != nil { + return err + } + + return site.Build() } diff --git a/commands/serve.go b/commands/serve.go index 26b7606..228192b 100644 --- a/commands/serve.go +++ b/commands/serve.go @@ -8,19 +8,24 @@ import ( "os" "path/filepath" + "github.com/facundoolano/blorg/config" "github.com/facundoolano/blorg/site" "github.com/fsnotify/fsnotify" ) // Generate and serve the site, rebuilding when the source files change. -func Serve() error { +func Serve(rootDir string) error { + config, err := config.LoadDevServer(rootDir) + if err != nil { + return err + } - if err := rebuild(); err != nil { + if err := rebuild(config); err != nil { return err } // watch for changes in src and layouts, and trigger a rebuild - watcher, err := setupWatcher() + watcher, err := setupWatcher(config) if err != nil { return err } @@ -35,13 +40,14 @@ func Serve() error { return nil } -func rebuild() error { - site, err := site.Load(SRC_DIR, LAYOUTS_DIR, DATA_DIR) +func rebuild(config *config.Config) error { + + site, err := site.Load(*config) if err != nil { return err } - if err := site.Build(SRC_DIR, TARGET_DIR, false, true); err != nil { + if err := site.Build(); err != nil { return err } @@ -66,7 +72,7 @@ func (d HTMLDir) Open(name string) (http.File, error) { return f, err } -func setupWatcher() (*fsnotify.Watcher, error) { +func setupWatcher(config *config.Config) (*fsnotify.Watcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err @@ -96,12 +102,12 @@ func setupWatcher() (*fsnotify.Watcher, error) { // since new nested directories could be triggering this change, and we need to watch those too // and since re-watching files is a noop, I just re-add the entire src everytime there's a change - if err := addAll(watcher); err != nil { + if err := addAll(watcher, config); err != nil { fmt.Println("error:", err) return } - if err := rebuild(); err != nil { + if err := rebuild(config); err != nil { fmt.Println("error:", err) return } @@ -115,19 +121,19 @@ func setupWatcher() (*fsnotify.Watcher, error) { } }() - err = addAll(watcher) + err = addAll(watcher, config) return watcher, err } // Add the layouts and all source directories to the given watcher -func addAll(watcher *fsnotify.Watcher) error { - err := watcher.Add(LAYOUTS_DIR) - err = watcher.Add(DATA_DIR) - err = watcher.Add(INCLUDES_DIR) +func addAll(watcher *fsnotify.Watcher, config *config.Config) error { + err := watcher.Add(config.LayoutsDir) + err = watcher.Add(config.DataDir) + err = watcher.Add(config.IncludesDir) // fsnotify watches all files within a dir, but non recursively // this walks through the src dir and adds watches for each found directory - filepath.WalkDir(SRC_DIR, func(path string, entry fs.DirEntry, err error) error { + filepath.WalkDir(config.SrcDir, func(path string, entry fs.DirEntry, err error) error { if entry.IsDir() { watcher.Add(path) } diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..ec4cefb --- /dev/null +++ b/config/config.go @@ -0,0 +1,116 @@ +package config + +import ( + "errors" + "fmt" + "maps" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +// The properties that are depended upon in the source code are declared explicitly in the config struct. +// The constructors will set default values for most. +// Depending on the command, different defaults will be used (serve is assumed to be a "dev" environment +// while build is assumed to be prod) +// Some defaults could be overridden by cli flags (eg disable live reload on serve). +// The user can override some of those via config yaml. +// The non declared values found in config yaml will just be passed as site.config values + +type Config struct { + RootDir string + SrcDir string + TargetDir string + LayoutsDir string + IncludesDir string + DataDir string + + SiteUrl string + SlugFormat string + + Minify bool + LiveReload bool + LinkStatic bool + + ServerHost string + ServerPort int + + pageDefaults map[string]interface{} + + // the user provided overrides, as found in config.yml + // these will passed as found as template context + overrides map[string]interface{} +} + +func Load(rootDir string) (*Config, error) { + // TODO allow to disable minify + + config := &Config{ + RootDir: rootDir, + SrcDir: filepath.Join(rootDir, "src"), + TargetDir: filepath.Join(rootDir, "target"), + LayoutsDir: filepath.Join(rootDir, "layouts"), + IncludesDir: filepath.Join(rootDir, "includes"), + DataDir: filepath.Join(rootDir, "data"), + SlugFormat: ":title", + Minify: true, + LiveReload: false, + LinkStatic: false, + pageDefaults: map[string]interface{}{}, + } + + // load overrides from config.yml + configPath := filepath.Join(rootDir, "config.yml") + yamlContent, err := os.ReadFile(configPath) + + if errors.Is(err, os.ErrNotExist) { + // config file is not mandatory + return config, nil + } else if err != nil { + return nil, err + } + + err = yaml.Unmarshal(yamlContent, &config.overrides) + if err != nil { + return nil, err + } + + // set user-provided overrides of declared config keys + if url, found := config.overrides["url"]; found { + config.SiteUrl = url.(string) + } + if slug, found := config.overrides["url"]; found { + config.SlugFormat = slug.(string) + } + + return config, nil +} + +func LoadDevServer(rootDir string) (*Config, error) { + // TODO revisit is this Load vs LoadDevServer is the best way to handle both modes + // TODO some of the options need to be overridable: host, port, live reload at least + + config, err := Load(rootDir) + if err != nil { + return nil, err + } + + // setup serve command specific overrides (these could be eventually tweaked with flags) + config.ServerHost = "localhost" + config.ServerPort = 4001 + config.Minify = false + config.LiveReload = true + config.LinkStatic = true + config.SiteUrl = fmt.Sprintf("http://%s:%d", config.ServerHost, config.ServerPort) + + return config, nil +} + +func (config Config) AsContext() map[string]interface{} { + context := map[string]interface{}{ + "url": config.SiteUrl, + } + maps.Copy(context, config.overrides) + return context +} diff --git a/go.mod b/go.mod index d1cdafc..32605da 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,17 @@ module github.com/facundoolano/blorg go 1.22.0 require ( + github.com/elliotchance/orderedmap/v2 v2.2.0 github.com/fsnotify/fsnotify v1.7.0 github.com/niklasfasching/go-org v1.7.0 github.com/osteele/liquid v1.3.2 github.com/yuin/goldmark v1.7.0 - gopkg.in/yaml.v3 v3.0.1 + golang.org/x/net v0.0.0-20201224014010-6772e930b67b + gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) require ( - github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect github.com/osteele/tuesday v1.0.3 // indirect - github.com/umpc/go-sortedmap v0.0.0-20180422175548-64ab94c482f4 // indirect - golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect golang.org/x/sys v0.4.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 544d455..c81bd77 100644 --- a/go.sum +++ b/go.sum @@ -12,11 +12,8 @@ github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/umpc/go-sortedmap v0.0.0-20180422175548-64ab94c482f4 h1:qk1XyC6UGfPa51PGmsTQJavyhfMLScqw97pEV3sFClI= -github.com/umpc/go-sortedmap v0.0.0-20180422175548-64ab94c482f4/go.mod h1:X6iKjXCleSyo/LZzKZ9zDF/ZB2L9gC36I5gLMf32w3M= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= @@ -31,5 +28,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 7082c10..79a6969 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,11 @@ func run(args []string) error { newCmd.Parse(os.Args[2:]) return commands.New() case "serve": - return commands.Serve() + rootDir := "." + if len(os.Args) > 2 { + rootDir = os.Args[2] + } + return commands.Serve(rootDir) default: // TODO print usage return errors.New("unknown subcommand") diff --git a/site/site.go b/site/site.go index c84b6a1..6b3a7f2 100644 --- a/site/site.go +++ b/site/site.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/facundoolano/blorg/config" "github.com/facundoolano/blorg/templates" "golang.org/x/net/html" "gopkg.in/yaml.v3" @@ -19,7 +20,7 @@ import ( const FILE_RW_MODE = 0777 type Site struct { - config map[string]string // may need to make this interface{} if config gets sophisticated + Config config.Config layouts map[string]templates.Template posts []map[string]interface{} pages []map[string]interface{} @@ -30,34 +31,33 @@ type Site struct { templates map[string]*templates.Template } -func Load(srcDir string, layoutsDir string, dataDir string) (*Site, error) { - // TODO load config from config.yml +func Load(config config.Config) (*Site, error) { site := Site{ layouts: make(map[string]templates.Template), templates: make(map[string]*templates.Template), - config: make(map[string]string), + Config: config, tags: make(map[string][]map[string]interface{}), data: make(map[string]interface{}), - templateEngine: templates.NewEngine(), + templateEngine: templates.NewEngine(config.SiteUrl), } - if err := site.loadDataFiles(dataDir); err != nil { + if err := site.loadDataFiles(); err != nil { return nil, err } - if err := site.loadLayouts(layoutsDir); err != nil { + if err := site.loadLayouts(); err != nil { return nil, err } - if err := site.loadTemplates(srcDir); err != nil { + if err := site.loadTemplates(); err != nil { return nil, err } return &site, nil } -func (site *Site) loadLayouts(layoutsDir string) error { - files, err := os.ReadDir(layoutsDir) +func (site *Site) loadLayouts() error { + files, err := os.ReadDir(site.Config.LayoutsDir) if os.IsNotExist(err) { return nil @@ -68,7 +68,7 @@ func (site *Site) loadLayouts(layoutsDir string) error { for _, entry := range files { if !entry.IsDir() { filename := entry.Name() - path := filepath.Join(layoutsDir, filename) + path := filepath.Join(site.Config.LayoutsDir, filename) templ, err := templates.Parse(site.templateEngine, path) if err != nil { return err @@ -82,8 +82,8 @@ func (site *Site) loadLayouts(layoutsDir string) error { return nil } -func (site *Site) loadDataFiles(dataDir string) error { - files, err := os.ReadDir(dataDir) +func (site *Site) loadDataFiles() error { + files, err := os.ReadDir(site.Config.DataDir) if os.IsNotExist(err) { return nil @@ -94,7 +94,7 @@ func (site *Site) loadDataFiles(dataDir string) error { for _, entry := range files { if !entry.IsDir() { filename := entry.Name() - path := filepath.Join(dataDir, filename) + path := filepath.Join(site.Config.DataDir, filename) yamlContent, err := os.ReadFile(path) if err != nil { @@ -114,15 +114,15 @@ func (site *Site) loadDataFiles(dataDir string) error { return nil } -func (site *Site) loadTemplates(srcDir string) error { - _, err := os.ReadDir(srcDir) +func (site *Site) loadTemplates() error { + _, err := os.ReadDir(site.Config.SrcDir) if os.IsNotExist(err) { - return fmt.Errorf("missing %s directory", srcDir) + return fmt.Errorf("missing %s directory", site.Config.SrcDir) } else if err != nil { - return fmt.Errorf("couldn't read %s", srcDir) + return fmt.Errorf("couldn't read %s", site.Config.SrcDir) } - err = filepath.WalkDir(srcDir, func(path string, entry fs.DirEntry, err error) error { + err = filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error { if !entry.IsDir() { templ, err := templates.Parse(site.templateEngine, path) // if something fails or this is not a template, skip @@ -131,10 +131,10 @@ func (site *Site) loadTemplates(srcDir string) error { } // set site related (?) metadata. Not sure if this should go elsewhere - relPath, _ := filepath.Rel(srcDir, path) + relPath, _ := filepath.Rel(site.Config.SrcDir, path) relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath)) + templ.Ext() templ.Metadata["path"] = relPath - templ.Metadata["url"] = "/" + strings.TrimSuffix(relPath, ".html") + templ.Metadata["url"] = "/" + strings.TrimSuffix(strings.TrimSuffix(relPath, "index.html"), ".html") templ.Metadata["dir"] = "/" + filepath.Dir(relPath) // posts are templates that can be chronologically sorted --that have a date. @@ -182,19 +182,18 @@ func (site *Site) loadTemplates(srcDir string) error { return nil } -// TODO consider making minify and reload site.config values -func (site *Site) Build(srcDir string, targetDir string, minify bool, htmlReload bool) error { +func (site *Site) Build() error { // clear previous target contents - os.RemoveAll(targetDir) - os.Mkdir(srcDir, FILE_RW_MODE) + os.RemoveAll(site.Config.TargetDir) + os.Mkdir(site.Config.SrcDir, FILE_RW_MODE) // walk the source directory, creating directories and files at the target dir - return filepath.WalkDir(srcDir, func(path string, entry fs.DirEntry, err error) error { + return filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } - subpath, _ := filepath.Rel(srcDir, path) - targetPath := filepath.Join(targetDir, subpath) + subpath, _ := filepath.Rel(site.Config.SrcDir, path) + targetPath := filepath.Join(site.Config.TargetDir, subpath) // if it's a directory, just create the same at the target if entry.IsDir() { @@ -202,7 +201,22 @@ func (site *Site) Build(srcDir string, targetDir string, minify bool, htmlReload } var contentReader io.Reader - if templ, found := site.templates[path]; found { + templ, found := site.templates[path] + if !found { + // if no template found at location, treat the file as static write its contents to target + if site.Config.LinkStatic { + // dev optimization: link static files instead of copying them + abs, _ := filepath.Abs(path) + return os.Symlink(abs, targetPath) + } + + srcFile, err := os.Open(path) + if err != nil { + return err + } + defer srcFile.Close() + contentReader = srcFile + } else { content, err := site.render(templ) if err != nil { return err @@ -210,25 +224,16 @@ func (site *Site) Build(srcDir string, targetDir string, minify bool, htmlReload targetPath = strings.TrimSuffix(targetPath, filepath.Ext(targetPath)) + templ.Ext() contentReader = bytes.NewReader(content) - } else { - // if no template found at location, treat the file as static - // write its contents to target - srcFile, err := os.Open(path) - if err != nil { - return err - } - defer srcFile.Close() - contentReader = srcFile } targetExt := filepath.Ext(targetPath) // if live reload is enabled, inject the reload snippet to html files - if htmlReload && targetExt == ".html" { + if site.Config.LiveReload && targetExt == ".html" { // TODO inject live reload snippet } // if enabled, minify web files - if minify && (targetExt == ".html" || targetExt == ".css" || targetExt == ".js") { + if site.Config.Minify && (targetExt == ".html" || targetExt == ".css" || targetExt == ".js") { // TODO minify output } @@ -241,7 +246,7 @@ func (site *Site) Build(srcDir string, targetDir string, minify bool, htmlReload func (site Site) render(templ *templates.Template) ([]byte, error) { ctx := map[string]interface{}{ "site": map[string]interface{}{ - "config": site.config, + "config": site.Config.AsContext(), "posts": site.posts, "tags": site.tags, "pages": site.pages, diff --git a/site/site_test.go b/site/site_test.go index 31dfb83..6a861f8 100644 --- a/site/site_test.go +++ b/site/site_test.go @@ -1,14 +1,15 @@ package site import ( + "github.com/facundoolano/blorg/config" "os" "path/filepath" "testing" ) func TestLoadAndRenderTemplates(t *testing.T) { - root, layouts, src, data := newProject() - defer os.RemoveAll(root) + config := newProject() + defer os.RemoveAll(config.RootDir) // add two layouts content := `--- @@ -19,7 +20,7 @@ func TestLoadAndRenderTemplates(t *testing.T) { {{content}} ` - file := newFile(layouts, "base.html", content) + file := newFile(config.LayoutsDir, "base.html", content) defer os.Remove(file.Name()) content = `--- @@ -28,7 +29,7 @@ layout: base

{{page.title}}

{{page.subtitle}}

{{content}}` - file = newFile(layouts, "post.html", content) + file = newFile(config.LayoutsDir, "post.html", content) defer os.Remove(file.Name()) // add two posts @@ -39,7 +40,7 @@ subtitle: my first post date: 2024-01-01 ---

Hello world!

` - file = newFile(src, "hello.html", content) + file = newFile(config.SrcDir, "hello.html", content) helloPath := file.Name() defer os.Remove(helloPath) @@ -50,7 +51,7 @@ subtitle: my last post date: 2024-02-01 ---

goodbye world!

` - file = newFile(src, "goodbye.html", content) + file = newFile(config.SrcDir, "goodbye.html", content) goodbyePath := file.Name() defer os.Remove(goodbyePath) @@ -60,15 +61,15 @@ layout: base title: about ---

about this site

` - file = newFile(src, "about.html", content) + file = newFile(config.SrcDir, "about.html", content) aboutPath := file.Name() defer os.Remove(aboutPath) // add a static file (no front matter) content = `go away!` - file = newFile(src, "robots.txt", content) + file = newFile(config.SrcDir, "robots.txt", content) - site, err := Load(src, layouts, data) + site, err := Load(*config) assertEqual(t, err, nil) @@ -115,15 +116,15 @@ title: about } func TestRenderArchive(t *testing.T) { - root, layouts, src, data := newProject() - defer os.RemoveAll(root) + config := newProject() + defer os.RemoveAll(config.RootDir) content := `--- title: hello world! date: 2024-01-01 ---

Hello world!

` - file := newFile(src, "hello.html", content) + file := newFile(config.SrcDir, "hello.html", content) defer os.Remove(file.Name()) content = `--- @@ -131,7 +132,7 @@ title: goodbye! date: 2024-02-01 ---

goodbye world!

` - file = newFile(src, "goodbye.html", content) + file = newFile(config.SrcDir, "goodbye.html", content) defer os.Remove(file.Name()) content = `--- @@ -139,7 +140,7 @@ title: an oldie! date: 2023-01-01 ---

oldie

` - file = newFile(src, "an-oldie.html", content) + file = newFile(config.SrcDir, "an-oldie.html", content) defer os.Remove(file.Name()) // add a page (no date) @@ -149,10 +150,10 @@ date: 2023-01-01
  • {{ post.date | date: "%Y-%m-%d" }} {{post.title}}
  • {%endfor%} ` - file = newFile(src, "about.html", content) + file = newFile(config.SrcDir, "about.html", content) defer os.Remove(file.Name()) - site, err := Load(src, layouts, data) + site, err := Load(*config) output, err := site.render(site.templates[file.Name()]) assertEqual(t, err, nil) assertEqual(t, string(output), `` - file = newFile(src, "index.html", content) + file = newFile(config.SrcDir, "index.html", content) defer os.Remove(file.Name()) - site, err := Load(src, layouts, data) + site, err := Load(*config) output, err := site.render(site.templates[file.Name()]) assertEqual(t, err, nil) assertEqual(t, string(output), `` - file = newFile(src, "projects.html", content) + file = newFile(config.SrcDir, "projects.html", content) defer os.Remove(file.Name()) - site, err := Load(src, layouts, data) + site, err := Load(*config) output, err := site.render(site.templates[file.Name()]) assertEqual(t, err, nil) assertEqual(t, string(output), `