Add more content for the serve command post (#23)

* document site api

* merge site.Load and site.Build into a single function

* fix tests

* make Site a private struct

* doc

* remove superfluous operation

* smaller tab width in source examples

* remove unnecessary temp var

* add some code snippet

* improve http fs naming

* add a few more code snippets

* cleanup serve code

* add watcher sample to blog post

* improve worker pool sample

* restore watcher close

* remove unused prefix

* some live reload samples in post

* remove outdated  command
This commit is contained in:
Facundo Olano 2024-03-03 23:19:09 -03:00 committed by GitHub
parent af09593474
commit ff28ece092
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 334 additions and 117 deletions

View file

@ -25,12 +25,7 @@ func (cmd *Build) Run(ctx *kong.Context) error {
return err
}
site, err := site.Load(*config)
if err != nil {
return err
}
return site.Build()
return site.Build(*config)
}
// Prompt the user for a string value

View file

@ -34,7 +34,7 @@ func (cmd *Serve) Run(ctx *kong.Context) error {
}
// watch for changes in src and layouts, and trigger a rebuild
watcher, broker, err := setupWatcher(config)
watcher, broker, err := runWatcher(config)
if err != nil {
return err
}
@ -42,7 +42,7 @@ func (cmd *Serve) Run(ctx *kong.Context) error {
// serve the target dir with a file server
fs := http.FileServer(HTMLFileSystem{http.Dir(config.TargetDir)})
http.Handle("/", http.StripPrefix("/", fs))
http.Handle("/", fs)
if config.LiveReload {
// handle client requests to listen to server-sent events
@ -83,11 +83,12 @@ func makeServerEventsHandler(broker *EventBroker) http.HandlerFunc {
// Sets up a watcher that will publish changes in the site source files
// to the returned event broker.
func setupWatcher(config *config.Config) (*fsnotify.Watcher, *EventBroker, error) {
func runWatcher(config *config.Config) (*fsnotify.Watcher, *EventBroker, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, nil, err
}
defer watchProjectFiles(watcher, config)
broker := newEventBroker()
@ -99,42 +100,27 @@ func setupWatcher(config *config.Config) (*fsnotify.Watcher, *EventBroker, error
})
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// chmod events are noisy, ignore them.
// Also ignore dot file events, which are usually spurious (e.g .DS_Store, emacs temp files)
isDotFile := strings.HasPrefix(filepath.Base(event.Name), ".")
if event.Has(fsnotify.Chmod) || isDotFile {
continue
}
// Schedule a rebuild to trigger after a delay. If there was another one pending
// it will be canceled.
fmt.Printf("\nfile %s changed\n", event.Name)
rebuildAfter.Stop()
rebuildAfter.Reset(100 * time.Millisecond)
case err, ok := <-watcher.Errors:
if !ok {
return
}
fmt.Println("error:", err)
for event := range watcher.Events {
// chmod events are noisy, ignore them.
// Also ignore dot file events, which are usually spurious (e.g .DS_Store, emacs temp files)
isDotFile := strings.HasPrefix(filepath.Base(event.Name), ".")
if event.Has(fsnotify.Chmod) || isDotFile {
continue
}
// Schedule a rebuild to trigger after a delay. If there was another one pending
// it will be canceled.
fmt.Printf("\nfile %s changed\n", event.Name)
rebuildAfter.Stop()
rebuildAfter.Reset(100 * time.Millisecond)
}
}()
err = addAll(watcher, config)
return watcher, broker, err
}
// Add the layouts and all source directories to the given watcher
func addAll(watcher *fsnotify.Watcher, config *config.Config) error {
// Configure the given watcher to notify for changes in the project source files
func watchProjectFiles(watcher *fsnotify.Watcher, config *config.Config) error {
watcher.Add(config.LayoutsDir)
watcher.Add(config.DataDir)
watcher.Add(config.IncludesDir)
@ -153,17 +139,11 @@ func rebuildSite(config *config.Config, watcher *fsnotify.Watcher, broker *Event
// 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, config); err != nil {
if err := watchProjectFiles(watcher, config); err != nil {
fmt.Println("couldn't add watchers:", err)
}
site, err := site.Load(*config)
if err != nil {
fmt.Println("load error:", err)
return
}
if err := site.Build(); err != nil {
if err := site.Build(*config); err != nil {
fmt.Println("build error:", err)
return
}
@ -176,15 +156,15 @@ func rebuildSite(config *config.Config, watcher *fsnotify.Watcher, broker *Event
// Tweaks the http file system to construct a server that hides the .html suffix from requests.
// Based on https://stackoverflow.com/a/57281956/993769
type HTMLFileSystem struct {
d http.Dir
dirFS http.Dir
}
func (d HTMLFileSystem) Open(name string) (http.File, error) {
func (htmlFS HTMLFileSystem) Open(name string) (http.File, error) {
// Try name as supplied
f, err := d.d.Open(name)
f, err := htmlFS.dirFS.Open(name)
if os.IsNotExist(err) {
// Not found, try with .html
if f, err := d.d.Open(name + ".html"); err == nil {
if f, err := htmlFS.dirFS.Open(name + ".html"); err == nil {
return f, nil
}
}

View file

@ -9,7 +9,7 @@ draft: true
#+OPTIONS: toc:nil num:1
#+LANGUAGE: en
** 1. Introduction
** Introduction
The core of the static site generator is the ~build~ command: take some input files, process them ---render templates, convert other markup formats into HTML--- and write the output for serving to the web. <This is where I started with ~jorge~, not only because it was the fundamental feature but because I needed to see the org-mode parsing output as early as possible to know whether I could reasonably expect this project to ultimately replace my Jekyll + org-export setup.
@ -20,27 +20,253 @@ I knew it was a feasible feature because other generators have it, but I didn't
The beauty of the ~serve~ command was that I could start with the most naive implementation and iterate towards the ideal one, keeping a usable command at every step. With ~build~ and ~serve~ out of the way, I'd be almost done with the project, the rest being nice to have features and UX improvements.
** 2. Context
** 3. Implementation
** Implementation
*** A basic file server
- basic fs server implementaion
#+begin_src go
func Serve(config config.Config) error {
// load and build the project
if err := site.Build(config); err != nil {
return err
}
// serve target with file server
fs := http.FileServer(http.Dir(config.TargetDir))
http.Handle("/", fs)
fmt.Println("server listening at http://localhost:4001/")
return http.ListenAndServe(":4001", nil)
}
#+end_src
- improve for directory and html handling
https://stackoverflow.com/a/57281956/993769
#+begin_src go
type HTMLFileSystem struct {
dirFS http.Dir
}
func (htmlFS HTMLFileSystem) Open(name string) (http.File, error) {
// Try name as supplied
f, err := htmlFS.dirFS.Open(name)
if os.IsNotExist(err) {
// Not found, try with .html
if f, err := htmlFS.dirFS.Open(name + ".html"); err == nil {
return f, nil
}
}
return f, err
}
#+end_src
#+begin_src go
fs := http.FileServer(HTMLFileSystem{http.Dir(config.TargetDir)})
http.Handle("/", fs)
#+end_src
*** Watching for changes
- fsnotify to trigger builds
- optimization: ln static files
#+begin_src go
func runWatcher(config *config.Config) {
watcher, _ := fsnotify.NewWatcher()
defer watchProjectFiles(watcher, config)
go func() {
for event := range watcher.Events {
fmt.Printf("\nfile %s changed, rebuilding site\n", event.Name)
// new src directories could be triggering this event
// so project files need to be re-added every time
watchProjectFiles(watcher, config)
site.Build(*config)
}
}()
}
// Configure the given watcher to notify for changes
// in the project source files
func watchProjectFiles(watcher *fsnotify.Watcher, config *config.Config) {
// 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 {
if entry.IsDir() {
watcher.Add(path)
}
return nil
})
}
#+end_src
*** Build optimizations
- optimization: worker pool
#+begin_src go
func (site *Site) Build() error {
// clear previous target contents
os.RemoveAll(site.Config.TargetDir)
// walk the source directory, creating directories and files at the target dir
return filepath.WalkDir(site.Config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
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() {
return os.MkdirAll(targetPath, FILE_RW_MODE)
}
// if it's a file render or copy it at the target
return site.buildFile(path, targetPath)
})
}
#+end_src
#+begin_src go
// Create a channel to send paths to build and a worker pool to handle them concurrently
func spawnBuildWorkers(site *site) (*sync.WaitGroup, chan string) {
var wg sync.WaitGroup
files := make(chan string, 20)
for range runtime.NumCPU() {
wg.Add(1)
go func(files <-chan string) {
defer wg.Done()
for path := range files {
site.buildFile(path)
}
}(files)
}
return &wg, files
}
#+end_src
#+begin_src diff
func (site *site) build() error {
// clear previous target contents
os.RemoveAll(site.Config.TargetDir)
+ wg, files := spawnBuildWorkers(site)
+ defer wg.Wait()
+ defer close(files)
// walk the source directory, creating directories and files at the target dir
return filepath.WalkDir(site.config.SrcDir, func(path string, entry fs.DirEntry, err error) error {
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() {
return os.MkdirAll(targetPath, FILE_RW_MODE)
}
- // if it's a file render or copy it at the target
- return site.buildFile(path, targetPath)
+ // if it's a file send the path to a worker
+ // to render or copy it at the target
+ files <- path
+ return nil
})
}
#+end_src
- optimization: ln static files
*** Live reload
- naive implementation
- event broker
- is this name right?
- intro sse (vs ws)
- sse boilerplate
*** Refinements
- don't stop on errors
- ignore chmod and temp file events
#+begin_src diff
fs := http.FileServer(HTMLFileSystem{http.Dir(config.TargetDir)})
http.Handle("/", fs)
+ http.Handle("/_events/", ServerEventsHandler)
#+end_src
#+begin_src go
func ServerEventsHandler (res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/event-stream")
res.Header().Set("Connection", "keep-alive")
res.Header().Set("Cache-Control", "no-cache")
res.Header().Set("Access-Control-Allow-Origin", "*")
for {
select {
case <-time.After(5 * time.Second):
// send an event to the connected client.
// data\n\n just means send an empty, unnamed event
fmt.Fprint(res, "data\n\n")
res.(http.Flusher).Flush()
case <-req.Context().Done():
// client connection closed
return
}
}
}
#+end_src
- client boilerplate
#+begin_src javascript
var eventSource;
function newSSE() {
console.log("connecting to server events");
eventSource = new EventSource('http://localhost:4001/_events/');
// when the server sends an event, refresh the page
eventSource.onmessage = function () {
location.reload()
};
// close connection before refreshing the page
window.onbeforeunload = function() {
eventSource.close();
}
// on errors disconnect and attempt reconnection after a delay
// this handles server restarting, laptop sleeping, etc.
eventSource.onerror = function (event) {
console.error('an error occurred:', event);
eventSource.close();
setTimeout(newSSE, 5000)
};
}
newSSE();
#+end_src
- event broker
- explain need
- is this name right?
- show api + link implementation
- show updated handler
- show updated watcher
#+begin_src diff
-func runWatcher(config *config.Config) {
+func runWatcher(config *config.Config) *EventBroker {
watcher, _ := fsnotify.NewWatcher()
defer watchProjectFiles(watcher, config)
+ broker := newEventBroker()
go func() {
for event := range watcher.Events {
fmt.Printf("\nfile %s changed, rebuilding site\n", event.Name)
// new src directories could be triggering this event
// so project files need to be re-added every time
watchProjectFiles(watcher, config)
site.Build(*config)
+ broker.publish("rebuild")
}
}()
+ return broker
}
#+end_src
- delay to prevent bursts

View file

@ -23,6 +23,7 @@ import (
const FM_SEPARATOR = "---"
const NO_SYNTAX_HIGHLIGHTING = ""
const CODE_TABWIDTH = 4
type Engine = liquid.Engine
@ -168,6 +169,7 @@ func (templ Template) RenderWith(context map[string]interface{}, hlTheme string)
if hlTheme != NO_SYNTAX_HIGHLIGHTING {
options = append(options, goldmark.WithExtensions(gm_highlight.NewHighlighting(
gm_highlight.WithStyle(hlTheme),
gm_highlight.WithFormatOptions(html.TabWidth(CODE_TABWIDTH)),
)))
}
md := goldmark.New(options...)
@ -190,7 +192,9 @@ func highlightCodeBlock(hlTheme string) func(source string, lang string, inline
}
l = chroma.Coalesce(l)
it, _ := l.Tokenise(nil, source)
options := []html.Option{}
options := []html.Option{
html.TabWidth(CODE_TABWIDTH),
}
if params[":hl_lines"] != "" {
ranges := org.ParseRanges(params[":hl_lines"])
if ranges != nil {

View file

@ -22,8 +22,8 @@ import (
const FILE_RW_MODE = 0666
const DIR_RWE_MODE = 0777
type Site struct {
Config config.Config
type site struct {
config config.Config
layouts map[string]markup.Template
posts []map[string]interface{}
pages []map[string]interface{}
@ -36,11 +36,25 @@ type Site struct {
minifier markup.Minifier
}
func Load(config config.Config) (*Site, error) {
site := Site{
// Load the site project pointed by `config`, then walk `config.SrcDir`
// and recreate it at `config.TargetDir` by rendering template files and copying static ones.
// The previous target dir contents are deleted.
func Build(config config.Config) error {
site, err := load(config)
if err != nil {
return err
}
return site.build()
}
// Create a new site instance by scanning the project directories
// pointed by `config`, loading layouts, templates and data files.
func load(config config.Config) (*site, error) {
site := site{
layouts: make(map[string]markup.Template),
templates: make(map[string]*markup.Template),
Config: config,
config: config,
tags: make(map[string][]map[string]interface{}),
data: make(map[string]interface{}),
templateEngine: markup.NewEngine(config.SiteUrl, config.IncludesDir),
@ -63,8 +77,8 @@ func Load(config config.Config) (*Site, error) {
return &site, nil
}
func (site *Site) loadLayouts() error {
files, err := os.ReadDir(site.Config.LayoutsDir)
func (site *site) loadLayouts() error {
files, err := os.ReadDir(site.config.LayoutsDir)
if os.IsNotExist(err) {
return nil
@ -75,7 +89,7 @@ func (site *Site) loadLayouts() error {
for _, entry := range files {
if !entry.IsDir() {
filename := entry.Name()
path := filepath.Join(site.Config.LayoutsDir, filename)
path := filepath.Join(site.config.LayoutsDir, filename)
templ, err := markup.Parse(site.templateEngine, path)
if err != nil {
return checkFileError(err)
@ -89,8 +103,8 @@ func (site *Site) loadLayouts() error {
return nil
}
func (site *Site) loadDataFiles() error {
files, err := os.ReadDir(site.Config.DataDir)
func (site *site) loadDataFiles() error {
files, err := os.ReadDir(site.config.DataDir)
if os.IsNotExist(err) {
return nil
@ -101,7 +115,7 @@ func (site *Site) loadDataFiles() error {
for _, entry := range files {
if !entry.IsDir() {
filename := entry.Name()
path := filepath.Join(site.Config.DataDir, filename)
path := filepath.Join(site.config.DataDir, filename)
yamlContent, err := os.ReadFile(path)
if err != nil {
@ -121,12 +135,12 @@ func (site *Site) loadDataFiles() error {
return nil
}
func (site *Site) loadTemplates() error {
if _, err := os.Stat(site.Config.SrcDir); err != nil {
func (site *site) loadTemplates() error {
if _, err := os.Stat(site.config.SrcDir); err != nil {
return fmt.Errorf("missing src directory")
}
err := filepath.WalkDir(site.Config.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 := markup.Parse(site.templateEngine, path)
// if something fails or this is not a template, skip
@ -135,8 +149,8 @@ func (site *Site) loadTemplates() error {
}
// set site related (?) metadata. Not sure if this should go elsewhere
relPath, _ := filepath.Rel(site.Config.SrcDir, path)
srcPath, _ := filepath.Rel(site.Config.RootDir, path)
relPath, _ := filepath.Rel(site.config.SrcDir, path)
srcPath, _ := filepath.Rel(site.config.RootDir, path)
relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath)) + templ.TargetExt()
templ.Metadata["src_path"] = srcPath
templ.Metadata["path"] = relPath
@ -145,7 +159,7 @@ func (site *Site) loadTemplates() error {
// if drafts are disabled, exclude from posts, page and tags indexes, but not from site.templates
// we want to explicitly exclude the template from the target, rather than treating it as a non template file
if !templ.IsDraft() || site.Config.IncludeDrafts {
if !templ.IsDraft() || site.config.IncludeDrafts {
// posts are templates that can be chronologically sorted --that have a date.
// the rest are pages.
if templ.IsPost() {
@ -205,9 +219,9 @@ func (site *Site) loadTemplates() error {
return nil
}
func (site *Site) addPrevNext(posts []map[string]interface{}) {
func (site *site) addPrevNext(posts []map[string]interface{}) {
for i, post := range posts {
path := filepath.Join(site.Config.RootDir, post["src_path"].(string))
path := filepath.Join(site.config.RootDir, post["src_path"].(string))
// only consider them part of the same collection if they share the directory
if i > 0 && post["dir"] == posts[i-1]["dir"] {
@ -227,23 +241,23 @@ func (site *Site) addPrevNext(posts []map[string]interface{}) {
}
}
func (site *Site) Build() error {
// Walk the `site.Config.SrcDir` directory and reproduce it at `site.Config.TargetDir`,
// rendering template files and copying static ones.
func (site *site) build() error {
// clear previous target contents
os.RemoveAll(site.Config.TargetDir)
os.Mkdir(site.Config.
SrcDir, DIR_RWE_MODE)
os.RemoveAll(site.config.TargetDir)
wg, files := spawnBuildWorkers(site)
defer wg.Wait()
defer close(files)
// walk the source directory, creating directories and files at the target dir
err := filepath.WalkDir(site.Config.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(site.Config.SrcDir, path)
targetPath := filepath.Join(site.Config.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() {
@ -253,12 +267,10 @@ func (site *Site) Build() error {
files <- path
return nil
})
return err
}
// Create a channel to send paths to build and a worker pool to handle them concurrently
func spawnBuildWorkers(site *Site) (*sync.WaitGroup, chan string) {
func spawnBuildWorkers(site *site) (*sync.WaitGroup, chan string) {
var wg sync.WaitGroup
files := make(chan string, 20)
@ -275,16 +287,16 @@ func spawnBuildWorkers(site *Site) (*sync.WaitGroup, chan string) {
return &wg, files
}
func (site *Site) buildFile(path string) error {
subpath, _ := filepath.Rel(site.Config.SrcDir, path)
targetPath := filepath.Join(site.Config.TargetDir, subpath)
func (site *site) buildFile(path string) error {
subpath, _ := filepath.Rel(site.config.SrcDir, path)
targetPath := filepath.Join(site.config.TargetDir, subpath)
var contentReader io.Reader
var err error
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 {
if site.config.LinkStatic {
// dev optimization: link static files instead of copying them
abs, _ := filepath.Abs(path)
err = os.Symlink(abs, targetPath)
@ -298,7 +310,7 @@ func (site *Site) buildFile(path string) error {
defer srcFile.Close()
contentReader = srcFile
} else {
if templ.IsDraft() && !site.Config.IncludeDrafts {
if templ.IsDraft() && !site.config.IncludeDrafts {
fmt.Println("skipping draft", targetPath)
return nil
}
@ -322,7 +334,7 @@ func (site *Site) buildFile(path string) error {
if err != nil {
return err
}
if site.Config.Minify {
if site.config.Minify {
contentReader = site.minifier.Minify(targetExt, contentReader)
}
@ -330,10 +342,10 @@ func (site *Site) buildFile(path string) error {
return writeToFile(targetPath, contentReader)
}
func (site *Site) render(templ *markup.Template) ([]byte, error) {
func (site *site) render(templ *markup.Template) ([]byte, error) {
ctx := map[string]interface{}{
"site": map[string]interface{}{
"config": site.Config.AsContext(),
"config": site.config.AsContext(),
"posts": site.posts,
"tags": site.tags,
"pages": site.pages,
@ -342,7 +354,7 @@ func (site *Site) render(templ *markup.Template) ([]byte, error) {
}
ctx["page"] = templ.Metadata
content, err := templ.RenderWith(ctx, site.Config.HighlightTheme)
content, err := templ.RenderWith(ctx, site.config.HighlightTheme)
if err != nil {
return nil, err
}
@ -353,7 +365,7 @@ func (site *Site) render(templ *markup.Template) ([]byte, error) {
if layout_templ, ok := site.layouts[layout.(string)]; ok {
ctx["layout"] = layout_templ.Metadata
ctx["content"] = content
content, err = layout_templ.RenderWith(ctx, site.Config.HighlightTheme)
content, err = layout_templ.RenderWith(ctx, site.config.HighlightTheme)
if err != nil {
return nil, err
}
@ -416,8 +428,8 @@ func getExcerpt(templ *markup.Template) string {
}
// if live reload is enabled, inject the reload snippet to html files
func (site *Site) injectLiveReload(extension string, contentReader io.Reader) (io.Reader, error) {
if !site.Config.LiveReload || extension != ".html" {
func (site *site) injectLiveReload(extension string, contentReader io.Reader) (io.Reader, error) {
if !site.config.LiveReload || extension != ".html" {
return contentReader, nil
}
@ -440,6 +452,6 @@ function newSSE() {
};
}
newSSE();`
script := fmt.Sprintf(JS_SNIPPET, site.Config.SiteUrl)
script := fmt.Sprintf(JS_SNIPPET, site.config.SiteUrl)
return markup.InjectScript(contentReader, script)
}

View file

@ -71,7 +71,7 @@ title: about
content = `go away!`
newFile(config.SrcDir, "robots.txt", content)
site, err := Load(*config)
site, err := load(*config)
assertEqual(t, err, nil)
@ -166,7 +166,7 @@ date: 2024-02-03
newFile(tutorial2, "another-entry.html", `---
---`)
site, err := Load(*config)
site, err := load(*config)
// helper method to map a filename to its prev next keys (if any)
getPrevNext := func(dir string, filename string) (interface{}, interface{}) {
path := filepath.Join(dir, filename)
@ -261,7 +261,7 @@ date: 2023-01-01
file = newFile(config.SrcDir, "about.html", content)
defer os.Remove(file.Name())
site, _ := Load(*config)
site, _ := load(*config)
output, err := site.render(site.templates[file.Name()])
assertEqual(t, err, nil)
assertEqual(t, string(output), `<ul>
@ -314,7 +314,7 @@ tags: [software]
file = newFile(config.SrcDir, "about.html", content)
defer os.Remove(file.Name())
site, _ := Load(*config)
site, _ := load(*config)
output, err := site.render(site.templates[file.Name()])
assertEqual(t, err, nil)
assertEqual(t, string(output), `<h1>software</h1>
@ -365,7 +365,7 @@ title: "2. an oldie!"
file = newFile(config.SrcDir, "index.html", content)
defer os.Remove(file.Name())
site, _ := Load(*config)
site, _ := load(*config)
output, err := site.render(site.templates[file.Name()])
assertEqual(t, err, nil)
assertEqual(t, string(output), `<ul>
@ -420,7 +420,7 @@ tags: [software]
file = newFile(config.SrcDir, "about.html", content)
defer os.Remove(file.Name())
site, _ := Load(*config)
site, _ := load(*config)
output, err := site.render(site.templates[file.Name()])
assertEqual(t, err, nil)
assertEqual(t, strings.TrimSpace(string(output)), `goodbye! - an overridden excerpt
@ -453,7 +453,7 @@ func TestRenderDataFile(t *testing.T) {
file = newFile(config.SrcDir, "projects.html", content)
defer os.Remove(file.Name())
site, _ := Load(*config)
site, _ := load(*config)
output, err := site.render(site.templates[file.Name()])
assertEqual(t, err, nil)
assertEqual(t, string(output), `<ul>
@ -505,9 +505,9 @@ layout: base
newFile(config.SrcDir, "index.html", content)
// build site
site, err := Load(*config)
site, err := load(*config)
assertEqual(t, err, nil)
err = site.Build()
err = site.build()
assertEqual(t, err, nil)
// test target files generated
@ -574,9 +574,9 @@ layout: base
// build site with drafts
config.IncludeDrafts = true
site, err := Load(*config)
site, err := load(*config)
assertEqual(t, err, nil)
err = site.Build()
err = site.build()
assertEqual(t, err, nil)
// test target files generated
@ -599,9 +599,9 @@ layout: base
// build site WITHOUT drafts
config.IncludeDrafts = false
site, err = Load(*config)
site, err = load(*config)
assertEqual(t, err, nil)
err = site.Build()
err = site.build()
assertEqual(t, err, nil)
// test only non drafts generated