2024 rewrite
Anirudh Oppiliappan x@icyphox.sh
Wed, 24 Jul 2024 00:10:52 +0300
11 files changed,
514 insertions(+),
419 deletions(-)
M
atom/feed.go
→
atom/feed.go
@@ -7,7 +7,7 @@ "path/filepath"
"time" "git.icyphox.sh/vite/config" - "git.icyphox.sh/vite/markdown" + "git.icyphox.sh/vite/types" ) type AtomLink struct {@@ -50,7 +50,7 @@ Entries []AtomEntry
} // Creates a new Atom feed. -func NewAtomFeed(srcDir string, posts []markdown.Output) ([]byte, error) { +func NewAtomFeed(srcDir string, posts []types.Post) ([]byte, error) { entries := []AtomEntry{} for _, p := range posts {@@ -76,7 +76,7 @@ Link: &AtomLink{Href: config.Config.URL + filepath.Join(srcDir, p.Meta["slug"])},
Summary: &AtomSummary{ Content: fmt.Sprintf("<h2>%s</h2>\n%s", p.Meta["subtitle"], - string(p.HTML)), + string(p.Body)), Type: "html", }, }
D
commands/build.go
@@ -1,318 +0,0 @@
-package commands - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" - "sync" - "time" - - "git.icyphox.sh/vite/atom" - "git.icyphox.sh/vite/config" - "git.icyphox.sh/vite/markdown" - "git.icyphox.sh/vite/util" - "gopkg.in/yaml.v3" -) - -const ( - BuildDir = "build" - PagesDir = "pages" - TemplatesDir = "templates" - StaticDir = "static" -) - -type Pages struct { - Dirs []string - Files []string -} - -// Populates a Pages object with dirs and files -// found in 'pages/'. -func (pgs *Pages) initPages() error { - files, err := os.ReadDir("./pages") - if err != nil { - return err - } - - for _, f := range files { - if f.IsDir() { - pgs.Dirs = append(pgs.Dirs, f.Name()) - } else { - pgs.Files = append(pgs.Files, f.Name()) - } - } - - return nil -} - -func (pgs *Pages) processFiles() error { - for _, f := range pgs.Files { - switch filepath.Ext(f) { - case ".md": - // ex: pages/about.md - mdFile := filepath.Join(PagesDir, f) - var htmlDir string - // ex: build/index.html (root index) - if f == "_index.md" { - htmlDir = BuildDir - } else { - htmlDir = filepath.Join( - BuildDir, - strings.TrimSuffix(f, ".md"), - ) - } - os.Mkdir(htmlDir, 0755) - // ex: build/about/index.html - htmlFile := filepath.Join(htmlDir, "index.html") - - fb, err := os.ReadFile(mdFile) - if err != nil { - return err - } - - out := markdown.Output{} - if err = out.RenderMarkdown(fb); err != nil { - return err - } - if err = out.RenderHTML( - htmlFile, - TemplatesDir, - struct { - Cfg config.ConfigYaml - Meta markdown.Matter - Body string - }{config.Config, out.Meta, string(out.HTML)}, - ); err != nil { - return err - } - case ".yaml": - // ex: pages/reading.yaml - yamlFile := filepath.Join(PagesDir, f) - htmlDir := filepath.Join(BuildDir, strings.TrimSuffix(f, ".yaml")) - os.Mkdir(htmlDir, 0755) - htmlFile := filepath.Join(htmlDir, "index.html") - - yb, err := os.ReadFile(yamlFile) - if err != nil { - return err - } - - data := map[string]interface{}{} - err = yaml.Unmarshal(yb, &data) - if err != nil { - return fmt.Errorf("error: unmarshalling yaml file %s: %v", yamlFile, err) - } - - meta := make(map[string]string) - for k, v := range data["meta"].(map[string]interface{}) { - meta[k] = v.(string) - } - - out := markdown.Output{} - out.Meta = meta - if err = out.RenderHTML( - htmlFile, - TemplatesDir, - struct { - Cfg config.ConfigYaml - Meta markdown.Matter - Yaml map[string]interface{} - Body string - }{config.Config, meta, data, ""}, - ); err != nil { - return err - } - default: - src := filepath.Join(PagesDir, f) - dst := filepath.Join(BuildDir, f) - if err := util.CopyFile(src, dst); err != nil { - return err - } - } - } - return nil -} - -func (pgs *Pages) processDirs() error { - for _, d := range pgs.Dirs { - // ex: build/blog - dstDir := filepath.Join(BuildDir, d) - // ex: pages/blog - srcDir := filepath.Join(PagesDir, d) - os.Mkdir(dstDir, 0755) - - entries, err := os.ReadDir(srcDir) - if err != nil { - return err - } - - posts := []markdown.Output{} - // Collect all posts - for _, e := range entries { - // foo-bar.md -> foo-bar - slug := strings.TrimSuffix(e.Name(), filepath.Ext(e.Name())) - - // ex: build/blog/foo-bar/ - os.Mkdir(filepath.Join(dstDir, slug), 0755) - // ex: build/blog/foo-bar/index.html - htmlFile := filepath.Join(dstDir, slug, "index.html") - - if e.Name() != "_index.md" { - ePath := filepath.Join(srcDir, e.Name()) - fb, err := os.ReadFile(ePath) - if err != nil { - return err - } - - out := markdown.Output{} - if err := out.RenderMarkdown(fb); err != nil { - return err - } - if err = out.RenderHTML( - htmlFile, - TemplatesDir, - struct { - Cfg config.ConfigYaml - Meta markdown.Matter - Body string - }{config.Config, out.Meta, string(out.HTML)}, - ); err != nil { - return err - } - posts = append(posts, out) - } - - // Sort posts slice by date - sort.Slice(posts, func(i, j int) bool { - dateStr1 := posts[j].Meta["date"] - dateStr2 := posts[i].Meta["date"] - date1, _ := time.Parse("2006-01-02", dateStr1) - date2, _ := time.Parse("2006-01-02", dateStr2) - return date1.Before(date2) - }) - } - - // Render index using posts slice. - // ex: build/blog/index.html - indexHTML := filepath.Join(dstDir, "index.html") - // ex: pages/blog/_index.md - indexMd, err := os.ReadFile(filepath.Join(srcDir, "_index.md")) - if err != nil { - return err - } - out := markdown.Output{} - if err := out.RenderMarkdown(indexMd); err != nil { - return err - } - - out.RenderHTML(indexHTML, TemplatesDir, struct { - Cfg config.ConfigYaml - Meta markdown.Matter - Body string - Posts []markdown.Output - }{config.Config, out.Meta, string(out.HTML), posts}) - - // Create feeds - // ex: build/blog/feed.xml - xml, err := atom.NewAtomFeed(d, posts) - if err != nil { - return err - } - feedFile := filepath.Join(dstDir, "feed.xml") - os.WriteFile(feedFile, xml, 0755) - } - return nil -} - -// Core builder function. Converts markdown to html, -// copies over non .md files, etc. -func Build() error { - if err := preBuild(); err != nil { - return err - } - fmt.Print("vite: building... ") - pages := Pages{} - if err := pages.initPages(); err != nil { - return err - } - - // Clean the build directory. - if err := util.Clean(BuildDir); err != nil { - return err - } - - wg := sync.WaitGroup{} - wg.Add(2) - wgDone := make(chan bool) - - ec := make(chan error) - - // Deal with files. - // ex: pages/{_index,about,etc}.md - go func() { - err := pages.processFiles() - if err != nil { - ec <- err - } - wg.Done() - }() - - // Deal with dirs -- i.e. dirs of markdown files. - // ex: pages/{blog,travel}/*.md - go func() { - err := pages.processDirs() - if err != nil { - ec <- err - } - wg.Done() - }() - - go func() { - wg.Wait() - close(wgDone) - }() - - select { - case <-wgDone: - break - case err := <-ec: - close(ec) - return err - } - - // Copy the static directory into build - // ex: build/static/ - buildStatic := filepath.Join(BuildDir, StaticDir) - os.Mkdir(buildStatic, 0755) - if err := util.CopyDir(StaticDir, buildStatic); err != nil { - return err - } - fmt.Print("done\n") - - if err := postBuild(); err != nil { - return err - } - return nil -} - -func postBuild() error { - for _, cmd := range config.Config.PostBuild { - fmt.Println("vite: running post-build command:", cmd) - if err := util.RunCmd(cmd); err != nil { - return err - } - } - return nil -} - -func preBuild() error { - for _, cmd := range config.Config.PreBuild { - fmt.Println("vite: running pre-build command:", cmd) - if err := util.RunCmd(cmd); err != nil { - return err - } - } - return nil -}
A
commands/build/build.go
@@ -0,0 +1,222 @@
+package build + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "git.icyphox.sh/vite/atom" + "git.icyphox.sh/vite/config" + "git.icyphox.sh/vite/formats" + "git.icyphox.sh/vite/formats/markdown" + "git.icyphox.sh/vite/formats/yaml" + "git.icyphox.sh/vite/types" + "git.icyphox.sh/vite/util" +) + +type Dir struct { + Name string + HasIndex bool + Files []types.File +} + +type Pages struct { + Dirs []Dir + Files []types.File +} + +func NewPages() (*Pages, error) { + pages := &Pages{} + + entries, err := os.ReadDir(types.PagesDir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsDir() { + thingsDir := filepath.Join(types.PagesDir, entry.Name()) + dir := Dir{Name: entry.Name()} + things, err := os.ReadDir(thingsDir) + if err != nil { + return nil, err + } + + for _, thing := range things { + if thing.Name() == "_index.md" { + dir.HasIndex = true + continue + } + switch filepath.Ext(thing.Name()) { + case ".md": + path := filepath.Join(thingsDir, thing.Name()) + dir.Files = append(dir.Files, &markdown.Markdown{Path: path}) + case ".yaml": + path := filepath.Join(thingsDir, thing.Name()) + dir.Files = append(dir.Files, &yaml.YAML{Path: path}) + default: + fmt.Printf("warn: unrecognized filetype for file: %s\n", thing.Name()) + } + } + + pages.Dirs = append(pages.Dirs, dir) + } else { + path := filepath.Join(types.PagesDir, entry.Name()) + switch filepath.Ext(entry.Name()) { + case ".md": + pages.Files = append(pages.Files, &markdown.Markdown{Path: path}) + case ".yaml": + pages.Files = append(pages.Files, &yaml.YAML{Path: path}) + default: + pages.Files = append(pages.Files, formats.Anything{Path: path}) + } + } + } + + return pages, nil +} + +// Build is the core builder function. Converts markdown/yaml +// to html, copies over non-.md/.yaml files, etc. +func Build() error { + if err := preBuild(); err != nil { + return err + } + fmt.Println("vite: building") + + pages, err := NewPages() + if err != nil { + return fmt.Errorf("error: reading 'pages/' %w", err) + } + + if err := util.Clean(types.BuildDir); err != nil { + return err + } + + if err := pages.ProcessFiles(); err != nil { + return err + } + + if err := pages.ProcessDirectories(); err != nil { + return err + } + + buildStatic := filepath.Join(types.BuildDir, types.StaticDir) + if err := os.MkdirAll(buildStatic, 0755); err != nil { + return err + } + if err := util.CopyDir(types.StaticDir, buildStatic); err != nil { + return err + } + fmt.Println("done") + + return nil +} + +// ProcessFiles handles root level files under 'pages', +// for example: 'pages/_index.md' or 'pages/about.md'. +func (p *Pages) ProcessFiles() error { + for _, f := range p.Files { + var htmlDir string + if f.Basename() == "_index.md" { + htmlDir = types.BuildDir + } else { + htmlDir = filepath.Join(types.BuildDir, strings.TrimSuffix(f.Basename(), f.Ext())) + } + + destFile := filepath.Join(htmlDir, "index.html") + if f.Ext() == "" { + destFile = filepath.Join(types.BuildDir, f.Basename()) + } else { + if err := os.MkdirAll(htmlDir, 0755); err != nil { + return err + } + } + if err := f.Render(destFile, nil); err != nil { + return fmt.Errorf("error: failed to render %s: %w", destFile, err) + } + } + return nil +} + +// ProcessDirectories handles directories of posts under 'pages', +// for example: 'pages/photos/foo.md' or 'pages/blog/bar.md'. +func (p *Pages) ProcessDirectories() error { + for _, dir := range p.Dirs { + dstDir := filepath.Join(types.BuildDir, dir.Name) + if err := os.MkdirAll(dstDir, 0755); err != nil { + return fmt.Errorf("error: failed to create directory: %s: %w", dstDir, err) + } + + posts := []types.Post{} + + for _, file := range dir.Files { + post := types.Post{} + // foo-bar.md -> foo-bar + slug := strings.TrimSuffix(file.Basename(), file.Ext()) + dstFile := filepath.Join(dstDir, slug, "index.html") + + // ex: build/blog/foo-bar/ + if err := os.MkdirAll(filepath.Join(dstDir, slug), 0755); err != nil { + return fmt.Errorf("error: failed to create directory: %s: %w", dstDir, err) + } + + if err := file.Render(dstFile, nil); err != nil { + return fmt.Errorf("error: failed to render %s: %w", dstFile, err) + } + + post.Meta = file.Frontmatter() + post.Body = file.Body() + posts = append(posts, post) + } + + sort.Slice(posts, func(i, j int) bool { + dateStr1 := posts[j].Meta["date"] + dateStr2 := posts[i].Meta["date"] + date1, _ := time.Parse("2006-01-02", dateStr1) + date2, _ := time.Parse("2006-01-02", dateStr2) + return date1.Before(date2) + }) + + if dir.HasIndex { + indexMd := filepath.Join(types.PagesDir, dir.Name, "_index.md") + index := markdown.Markdown{Path: indexMd} + dstFile := filepath.Join(dstDir, "index.html") + if err := index.Render(dstFile, posts); err != nil { + return fmt.Errorf("error: failed to render index %s: %w", dstFile, err) + } + } + + xml, err := atom.NewAtomFeed(filepath.Join(types.PagesDir, dir.Name), posts) + if err != nil { + return fmt.Errorf("error: failed to create atom feed for: %s: %w", dir.Name, err) + } + feedFile := filepath.Join(dstDir, "feed.xml") + os.WriteFile(feedFile, xml, 0755) + } + + return nil +} + +func postBuild() error { + for _, cmd := range config.Config.PostBuild { + fmt.Println("vite: running post-build command:", cmd) + if err := util.RunCmd(cmd); err != nil { + return err + } + } + return nil +} + +func preBuild() error { + for _, cmd := range config.Config.PreBuild { + fmt.Println("vite: running pre-build command:", cmd) + if err := util.RunCmd(cmd); err != nil { + return err + } + } + return nil +}
A
formats/anything.go
@@ -0,0 +1,19 @@
+package formats + +import ( + "path/filepath" + + "git.icyphox.sh/vite/util" +) + +// Anything is a stub format for unrecognized files +type Anything struct{ Path string } + +func (Anything) Ext() string { return "" } +func (Anything) Frontmatter() map[string]string { return nil } +func (Anything) Body() string { return "" } +func (a Anything) Basename() string { return filepath.Base(a.Path) } + +func (a Anything) Render(dest string, data interface{}) error { + return util.CopyFile(a.Path, dest) +}
A
formats/markdown/markdown.go
@@ -0,0 +1,128 @@
+package markdown + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + gotmpl "text/template" + "time" + + "git.icyphox.sh/vite/config" + "git.icyphox.sh/vite/template" + "git.icyphox.sh/vite/types" + "github.com/adrg/frontmatter" + + bf "git.icyphox.sh/grayfriday" +) + +var ( + bfFlags = bf.UseXHTML | bf.Smartypants | bf.SmartypantsFractions | + bf.SmartypantsDashes | bf.NofollowLinks | bf.FootnoteReturnLinks + bfExts = bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Autolink | + bf.Strikethrough | bf.SpaceHeadings | bf.BackslashLineBreak | + bf.AutoHeadingIDs | bf.HeadingIDs | bf.Footnotes | bf.NoEmptyLineBeforeBlock +) + +type Markdown struct { + body []byte + frontmatter map[string]string + Path string +} + +func (*Markdown) Ext() string { return ".md" } + +func (md *Markdown) Basename() string { + return filepath.Base(md.Path) +} + +// mdToHtml renders source markdown to html +func mdToHtml(source []byte) []byte { + return bf.Run( + source, + bf.WithNoExtensions(), + bf.WithRenderer(bf.NewHTMLRenderer(bf.HTMLRendererParameters{Flags: bfFlags})), + bf.WithExtensions(bfExts), + ) +} + +// template checks the frontmatter for a specified template or falls back +// to the default template -- to which it, well, templates whatever is in +// data and writes it to dest. +func (md *Markdown) template(dest, tmplDir string, data interface{}) error { + metaTemplate := md.frontmatter["template"] + if metaTemplate == "" { + metaTemplate = config.Config.DefaultTemplate + } + + tmpl := template.NewTmpl() + tmpl.SetFuncs(gotmpl.FuncMap{ + "parsedate": func(s string) time.Time { + date, _ := time.Parse("2006-01-02", s) + return date + }, + }) + if err := tmpl.Load(tmplDir); err != nil { + return err + } + + w, err := os.Create(dest) + if err != nil { + return err + } + + if err = tmpl.ExecuteTemplate(w, metaTemplate, data); err != nil { + return err + } + return nil +} + +// extract takes the source markdown page, extracts the frontmatter +// and body. The body is converted from markdown to html here. +func (md *Markdown) extractFrontmatter(source []byte) error { + r := bytes.NewReader(source) + rest, err := frontmatter.Parse(r, &md.frontmatter) + if err != nil { + return err + } + md.body = mdToHtml(rest) + return nil +} + +func (md *Markdown) Frontmatter() map[string]string { + return md.frontmatter +} + +func (md *Markdown) Body() string { + return string(md.body) +} + +type templateData struct { + Cfg config.ConfigYaml + Meta map[string]string + Body string + Extra interface{} +} + +func (md *Markdown) Render(dest string, data interface{}) error { + source, err := os.ReadFile(md.Path) + if err != nil { + return fmt.Errorf("markdown: error reading file: %w", err) + } + + err = md.extractFrontmatter(source) + if err != nil { + return fmt.Errorf("markdown: error extracting frontmatter: %w", err) + } + + err = md.template(dest, types.TemplatesDir, templateData{ + config.Config, + md.frontmatter, + string(md.body), + data, + }) + if err != nil { + return fmt.Errorf("markdown: failed to render to destination %s: %w", dest, err) + } + return nil +}
A
formats/yaml/yaml.go
@@ -0,0 +1,110 @@
+package yaml + +import ( + "fmt" + "os" + "path/filepath" + gotmpl "text/template" + "time" + + "git.icyphox.sh/vite/config" + "git.icyphox.sh/vite/template" + "git.icyphox.sh/vite/types" + "gopkg.in/yaml.v3" +) + +type YAML struct { + Path string + + meta map[string]string +} + +func (*YAML) Ext() string { return ".yaml" } +func (*YAML) Body() string { return "" } +func (y *YAML) Basename() string { return filepath.Base(y.Path) } + +func (y *YAML) Frontmatter() map[string]string { + return y.meta +} + +type templateData struct { + Cfg config.ConfigYaml + Meta map[string]string + Yaml map[string]interface{} + Body string +} + +func (y *YAML) template(dest, tmplDir string, data interface{}) error { + metaTemplate := y.meta["template"] + if metaTemplate == "" { + metaTemplate = config.Config.DefaultTemplate + } + + tmpl := template.NewTmpl() + tmpl.SetFuncs(gotmpl.FuncMap{ + "parsedate": func(s string) time.Time { + date, _ := time.Parse("2006-01-02", s) + return date + }, + }) + if err := tmpl.Load(tmplDir); err != nil { + return err + } + + w, err := os.Create(dest) + if err != nil { + return err + } + + if err = tmpl.ExecuteTemplate(w, metaTemplate, data); err != nil { + return err + } + return nil +} + +func (y *YAML) Render(dest string, data interface{}) error { + yamlBytes, err := os.ReadFile(y.Path) + if err != nil { + return fmt.Errorf("yaml: failed to read file: %s: %w", y.Path, err) + } + + yamlData := map[string]interface{}{} + err = yaml.Unmarshal(yamlBytes, yamlData) + if err != nil { + return fmt.Errorf("yaml: failed to unmarshal yaml file: %s: %w", y.Path, err) + } + + metaInterface := yamlData["meta"].(map[string]interface{}) + + meta := make(map[string]string) + for k, v := range metaInterface { + vStr := convertToString(v) + meta[k] = vStr + } + + y.meta = meta + + err = y.template(dest, types.TemplatesDir, templateData{ + config.Config, + y.meta, + yamlData, + "", + }) + if err != nil { + return fmt.Errorf("yaml: failed to render to destination %s: %w", dest, err) + } + + return nil +} + +func convertToString(value interface{}) string { + // Infer type and convert to string + switch v := value.(type) { + case string: + return v + case time.Time: + return v.Format("2006-01-02") + default: + return fmt.Sprintf("%v", v) + } +}
M
main.go
→
main.go
@@ -5,6 +5,7 @@ "fmt"
"os" "git.icyphox.sh/vite/commands" + "git.icyphox.sh/vite/commands/build" ) func main() {@@ -38,7 +39,7 @@ fmt.Fprintf(os.Stderr, "error: init: %+v\n", err)
} case "build": - if err := commands.Build(); err != nil { + if err := build.Build(); err != nil { fmt.Fprintf(os.Stderr, "error: build: %+v\n", err) }
D
markdown/frontmatter.go
@@ -1,24 +0,0 @@
-package markdown - -import ( - "bytes" - - "github.com/adrg/frontmatter" -) - -type Matter map[string]string - -type MarkdownDoc struct { - Frontmatter Matter - Body []byte -} - -func (md *MarkdownDoc) Extract(source []byte) error { - r := bytes.NewReader(source) - rest, err := frontmatter.Parse(r, &md.Frontmatter) - if err != nil { - return err - } - md.Body = rest - return nil -}
D
markdown/markdown.go
@@ -1,73 +0,0 @@
-package markdown - -import ( - "fmt" - "os" - gotmpl "text/template" - "time" - - "git.icyphox.sh/vite/config" - "git.icyphox.sh/vite/markdown/template" - - bf "git.icyphox.sh/grayfriday" -) - -var ( - bfFlags = bf.UseXHTML | bf.Smartypants | bf.SmartypantsFractions | - bf.SmartypantsDashes | bf.NofollowLinks | bf.FootnoteReturnLinks - bfExts = bf.NoIntraEmphasis | bf.Tables | bf.FencedCode | bf.Autolink | - bf.Strikethrough | bf.SpaceHeadings | bf.BackslashLineBreak | - bf.AutoHeadingIDs | bf.HeadingIDs | bf.Footnotes | bf.NoEmptyLineBeforeBlock -) - -type Output struct { - HTML []byte - Meta Matter -} - -// Renders markdown to html, and fetches metadata. -func (out *Output) RenderMarkdown(source []byte) error { - md := MarkdownDoc{} - if err := md.Extract(source); err != nil { - return fmt.Errorf("markdown: %w", err) - } - - out.HTML = bf.Run( - md.Body, - bf.WithNoExtensions(), - bf.WithRenderer(bf.NewHTMLRenderer(bf.HTMLRendererParameters{Flags: bfFlags})), - bf.WithExtensions(bfExts), - ) - out.Meta = md.Frontmatter - return nil -} - -// Renders out.HTML into dst html file, using the template specified -// in the frontmatter. data is the template struct. -func (out *Output) RenderHTML(dst, tmplDir string, data interface{}) error { - metaTemplate := out.Meta["template"] - if metaTemplate == "" { - metaTemplate = config.Config.DefaultTemplate - } - - tmpl := template.NewTmpl() - tmpl.SetFuncs(gotmpl.FuncMap{ - "parsedate": func(s string) time.Time { - date, _ := time.Parse("2006-01-02", s) - return date - }, - }) - if err := tmpl.Load(tmplDir); err != nil { - return err - } - - w, err := os.Create(dst) - if err != nil { - return err - } - - if err = tmpl.ExecuteTemplate(w, metaTemplate, data); err != nil { - return err - } - return nil -}
A
types/types.go
@@ -0,0 +1,30 @@
+package types + +const ( + BuildDir = "build" + PagesDir = "pages" + TemplatesDir = "templates" + StaticDir = "static" +) + +type File interface { + Ext() string + // Render takes any arbitrary data and combines that with the global config, + // page frontmatter and the body, as template params. Templates are read + // from types.TemplateDir and the final html is written to dest, + // with necessary directories being created. + Render(dest string, data interface{}) error + + // Frontmatter will not be populated if Render hasn't been called. + Frontmatter() map[string]string + // Body will not be populated if Render hasn't been called. + Body() string + Basename() string +} + +// Only used for building indexes and Atom feeds +type Post struct { + Meta map[string]string + // HTML-formatted body of post + Body string +}