diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..d3f3f65
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,48 @@
+// trofaf is a super-simple *live* static blog engine.
+//
+// Install using: `go get github.com/PuerkitoBio/trofaf`
+//
+// ## Description
+//
+// It generates a static website from *markdown* files and front matter, and requires only a simple
+// 3-directories structure to get going. It favors simplicity over features.
+//
+// To get started, create the 3 subdirectories (you can look at the `example/` subdirectory
+// for... an example):
+// * posts
+// * public
+// * templates
+//
+// trofaf only cares about `*.md` files in the posts directory, and about `*.amber` (Amber templates)
+// or `*.html` (native Go templates) files in the templates directory. It will watch for changes,
+// creates or deletes on those files in these directories, and will re-generate automatically
+// the website when required. This is the *live* part.
+//
+// All files in the public directory are exposed by the web server. Posts in markdown format get
+// translated to static html files at the root of the public directory. The html file name is
+// an URL-friendly slug generated from the original markdown file name. There is no extension, so
+// the URL looks clean and, uh, *modern*?
+//
+// An RSS feed is automatically generated from a number of recent posts, and saved as a static
+// XML file in the public directory.
+//
+// There is no special template for an index page, the most recent post (based on the publication
+// date found in the front matter of the markdown files) is saved twice - once under its own
+// html file, once under the index.html file, so that this is the page displayed when the root
+// of the web server is requested.
+//
+// When the site is (re-)generated, the public directory must be cleaned, because some posts may
+// have been deleted. Subdirectories are left untouched (so that `css/` or `js/` directories can
+// coexist peacefully), as well as hidden (dot) files, and some special files are also graced
+// from the destruction (robots.txt, favicon.ico, etc., see gen.go).
+//
+// ## Command-line Options
+//
+// The following options can be set at the command-line:
+// * Port (-p) : the port number for the web server, defaults to 9000.
+// * NoGen (-G) : prevents watching and live-generating the site. This is equivalent to running the static public directory.
+// * SiteName (-n) : the name of the web site, passed to the template.
+// * TagLine (-t) : a tag line for the web site, passed to the template.
+// * RecentPostsCount (-r) : the number of posts in the recent posts list, passed to the template and used for the RSS feed.
+// * BaseURL (-b) : the base URL of the web site, most likely the host name (i.e. http://www.mysite.com).
+package main
diff --git a/example/posts/003-dependances.md b/example/posts/003-dependances.md
index f28d8a1..c6846cf 100644
--- a/example/posts/003-dependances.md
+++ b/example/posts/003-dependances.md
@@ -1,5 +1,5 @@
---
-Date: 2012-03-05
+Date: 2012-03-01
Title: npm: la base essentielle pour débuter avec node.js
Author: Martin Angers
Category: technologie
diff --git a/example/posts/004-npm-packages.md b/example/posts/004-npm-packages.md
index c02a546..51693ac 100644
--- a/example/posts/004-npm-packages.md
+++ b/example/posts/004-npm-packages.md
@@ -1,5 +1,5 @@
---
-Date: 2012-03-12
+Date: 2012-03-01
Title: npm shrinkwrap: Comment contrôler ses dépendances
Author: Martin Angers
Category: technologie
diff --git a/example/posts/foo.txt b/example/posts/foo.txt
new file mode 100644
index 0000000..a8b76de
--- /dev/null
+++ b/example/posts/foo.txt
@@ -0,0 +1 @@
+test2!
diff --git a/example/public/002-meta-billet b/example/public/002-meta-billet
index 868de74..539d98e 100644
--- a/example/public/002-meta-billet
+++ b/example/public/002-meta-billet
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/003-dependances b/example/public/003-dependances
index 9e8bf04..52abd4e 100644
--- a/example/public/003-dependances
+++ b/example/public/003-dependances
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/004-npm-packages b/example/public/004-npm-packages
index 83dab7f..0b376f4 100644
--- a/example/public/004-npm-packages
+++ b/example/public/004-npm-packages
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/005-structurer-express b/example/public/005-structurer-express
index cb358de..37754ba 100644
--- a/example/public/005-structurer-express
+++ b/example/public/005-structurer-express
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/006-dependency-injection b/example/public/006-dependency-injection
index 8aa94d3..4dcd4f6 100644
--- a/example/public/006-dependency-injection
+++ b/example/public/006-dependency-injection
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/007-implement-js b/example/public/007-implement-js
index 21c3f76..f149ae5 100644
--- a/example/public/007-implement-js
+++ b/example/public/007-implement-js
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/008-backbone-computed-properties b/example/public/008-backbone-computed-properties
index 32f253d..108e390 100644
--- a/example/public/008-backbone-computed-properties
+++ b/example/public/008-backbone-computed-properties
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/009-je-men-vais b/example/public/009-je-men-vais
index de189c4..1d93146 100644
--- a/example/public/009-je-men-vais
+++ b/example/public/009-je-men-vais
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/010-demenagement b/example/public/010-demenagement
index bbb877e..326a1cf 100644
--- a/example/public/010-demenagement
+++ b/example/public/010-demenagement
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/index.html b/example/public/index.html
index bbb877e..326a1cf 100644
--- a/example/public/index.html
+++ b/example/public/index.html
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/j-42-preparation b/example/public/j-42-preparation
index 81a9710..74226ad 100644
--- a/example/public/j-42-preparation
+++ b/example/public/j-42-preparation
@@ -10,15 +10,15 @@
-
+
diff --git a/example/public/rss b/example/public/rss
index 42e4ab1..61e7bc1 100644
--- a/example/public/rss
+++ b/example/public/rss
@@ -1,2 +1,2 @@
-14 Jul 13 19:43 -0400 trofaf Ce blogue a déménagé sur GitHub /010-demenagementSuite à l'annonce de la fermeture de Calepin.co, j'ai déménagé ce blogue sur http://hypermegatop.github.com. Veuillez mettre à jour vos favoris! Martin Angers 30 Apr 12 00:00 +0000 Je suis venu te dire que je m'en vais /009-je-men-vaisOu plutôt, "je suis venu te dire qu'on m'évince", mais ça "punch" moins... En effet, mon coup de foudre pour le blogiciel Calepin aura finalement été son baiser de la mort. Sur Twitter la semaine dernière, via le canal officiel du site, on apprenait que c'était la fin des haricots. Martin Angers 23 Apr 12 00:00 +0000 Propriétés calculées avec Backbone /008-backbone-computed-propertiesLa beauté de l'univers du code libre est que lorsqu'il manque une fonctionnalité, on peut se retrousser les manches, ouvrir le code et l'ajouter. Voici ma petite histoire des propriétés calculées avec Backbone. Martin Angers 13 Apr 12 00:00 +0000 implement.js: typage fort et Javascript /007-implement-jsL'injection de dépendance avec Javascript a comme conséquence de ne pouvoir assumer que les fonctionnalités offertes par l'instance reçue seront celles attendues. Martin Angers 02 Apr 12 00:00 +0000 Expérimentations sur l'injection de dépendance avec node.js /006-dependency-injectionL'injection de dépendance permet de découpler les différentes composantes d'une application pour en faciliter l'entretien, la testabilité, circonscrire l'impact des changements, mais aussi pour imposer une façon d'aborder la création de l'application en une aggrégation de pièces simples, à la responsabilité ciblée, et à l'API bien défini. Martin Angers 27 Mar 12 00:00 +0000
\ No newline at end of file
+Ø value a wysiwyg hypertext cybersite http://localhost14 Jul 13 20:36 -0400 trofaf Ce blogue a déménagé sur GitHub http://localhost/010-demenagementSuite à l'annonce de la fermeture de Calepin.co, j'ai déménagé ce blogue sur http://hypermegatop.github.com. Veuillez mettre à jour vos favoris! Martin Angers 30 Apr 12 00:00 +0000 Je suis venu te dire que je m'en vais http://localhost/009-je-men-vaisOu plutôt, "je suis venu te dire qu'on m'évince", mais ça "punch" moins... En effet, mon coup de foudre pour le blogiciel Calepin aura finalement été son baiser de la mort. Sur Twitter la semaine dernière, via le canal officiel du site, on apprenait que c'était la fin des haricots. Martin Angers 23 Apr 12 00:00 +0000 Propriétés calculées avec Backbone http://localhost/008-backbone-computed-propertiesLa beauté de l'univers du code libre est que lorsqu'il manque une fonctionnalité, on peut se retrousser les manches, ouvrir le code et l'ajouter. Voici ma petite histoire des propriétés calculées avec Backbone. Martin Angers 13 Apr 12 00:00 +0000 implement.js: typage fort et Javascript http://localhost/007-implement-jsL'injection de dépendance avec Javascript a comme conséquence de ne pouvoir assumer que les fonctionnalités offertes par l'instance reçue seront celles attendues. Martin Angers 02 Apr 12 00:00 +0000 Expérimentations sur l'injection de dépendance avec node.js http://localhost/006-dependency-injectionL'injection de dépendance permet de découpler les différentes composantes d'une application pour en faciliter l'entretien, la testabilité, circonscrire l'impact des changements, mais aussi pour imposer une façon d'aborder la création de l'application en une aggrégation de pièces simples, à la responsabilité ciblée, et à l'API bien défini. Martin Angers 27 Mar 12 00:00 +0000
\ No newline at end of file
diff --git a/gen.go b/gen.go
index 5092014..1a51f8f 100644
--- a/gen.go
+++ b/gen.go
@@ -5,6 +5,7 @@ import (
"html/template"
"io"
"io/ioutil"
+ "log"
"net/url"
"os"
"path/filepath"
@@ -15,9 +16,8 @@ import (
)
var (
- postTpl *template.Template
- postTplNm = "post.amber"
- rssTplNm = "rss.amber"
+ postTpl *template.Template // The one and only compiled post template
+ postTplNm = "post.amber" // The amber post template file name (native Go are compiled using ParseGlob)
// Special files in the public directory, that must not be deleted
// If value is true, this must match the prefix of the file (HasPrefix())
@@ -35,12 +35,14 @@ var (
}
)
+// This type is a slice of *LongPost that implements the sort.Interface, to sort in PubTime order.
type sortableLongPost []*LongPost
func (s sortableLongPost) Len() int { return len(s) }
func (s sortableLongPost) Less(i, j int) bool { return s[i].PubTime.Before(s[j].PubTime) }
func (s sortableLongPost) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+// Filter cleans the slice of FileInfo to leave only `.md` files (markdown)
func Filter(fi []os.FileInfo) []os.FileInfo {
for i := 0; i < len(fi); {
if fi[i].IsDir() || filepath.Ext(fi[i].Name()) != ".md" {
@@ -52,6 +54,7 @@ func Filter(fi []os.FileInfo) []os.FileInfo {
return fi
}
+// Compile the Post template.
func compileTemplate() error {
ap := filepath.Join(TemplatesDir, postTplNm)
if _, err := os.Stat(ap); os.IsNotExist(err) {
@@ -73,6 +76,7 @@ func compileTemplate() error {
return nil
}
+// Clear the public directory, ignoring special files, subdirectories, and hidden (dot) files.
func clearPublicDir() error {
// Clear the public directory, except subdirs and special files (favicon.ico)
fis, err := ioutil.ReadDir(PublicDir)
@@ -93,6 +97,7 @@ func clearPublicDir() error {
return nil
}
+// Generate the whole site.
func generateSite() error {
// First compile the template(s)
if err := compileTemplate(); err != nil {
@@ -107,9 +112,14 @@ func generateSite() error {
fis = Filter(fis)
// Get all posts.
- all := make(sortableLongPost, len(fis))
- for i, fi := range fis {
- all[i] = newLongPost(fi)
+ all := make(sortableLongPost, 0, len(fis))
+ for _, fi := range fis {
+ lp, err := newLongPost(fi)
+ if err == nil {
+ all = append(all, lp)
+ } else {
+ log.Printf("post skipped: %s; error: %s\n", fi.Name(), err)
+ }
}
// Then sort in reverse order (newer first)
sort.Sort(sort.Reverse(all))
@@ -131,6 +141,7 @@ func generateSite() error {
return generateRss(td)
}
+// Creates the rss feed from the recent posts.
func generateRss(td *TemplateData) error {
r := NewRss(td.SiteName, td.TagLine, Options.BaseURL)
base, err := url.Parse(Options.BaseURL)
@@ -147,6 +158,7 @@ func generateRss(td *TemplateData) error {
return r.WriteToFile(filepath.Join(PublicDir, "rss"))
}
+// Generate the static HTML file for the post identified by the index.
func generateFile(td *TemplateData, idx bool) error {
var w io.Writer
diff --git a/main.go b/main.go
index 501d401..649115d 100644
--- a/main.go
+++ b/main.go
@@ -2,12 +2,14 @@ package main
import (
"log"
+ "net/url"
"os"
"path/filepath"
"github.com/jessevdk/go-flags"
)
+// This structure holds the command-line options.
type options struct {
Port int `short:"p" long:"port" description:"the port to use for the web server" default:"9000"`
NoGen bool `short:"G" long:"no-generation" description:"when set, the site is not automatically generated"`
@@ -18,13 +20,16 @@ type options struct {
}
var (
- Options options
- PublicDir string
- PostsDir string
- TemplatesDir string
+ // The one and only Options parsed from the command-line
+ Options options
+
+ PublicDir string // Public directory path
+ PostsDir string // Posts directory path
+ TemplatesDir string // Templates directory path
+ RssURL string // The RSS feed URL, parsed only once and stored for convenience
)
-func main() {
+func init() {
// Initialize directories
pwd, err := os.Getwd()
if err != nil {
@@ -33,10 +38,25 @@ func main() {
PublicDir = filepath.Join(pwd, "public")
PostsDir = filepath.Join(pwd, "posts")
TemplatesDir = filepath.Join(pwd, "templates")
+}
+
+func storeRssURL() {
+ b, err := url.Parse(Options.BaseURL)
+ if err != nil {
+ log.Fatal("FATAL ", err)
+ }
+ r, err := b.Parse("/rss")
+ if err != nil {
+ log.Fatal("FATAL ", err)
+ }
+ RssURL = r.String()
+}
+func main() {
// Parse the flags
- _, err = flags.Parse(&Options)
+ _, err := flags.Parse(&Options)
if err == nil { // err != nil prints the usage automatically
+ storeRssURL()
if !Options.NoGen {
// Generate the site
if err := generateSite(); err != nil {
diff --git a/rss.go b/rss.go
index aefb2d1..92b56c5 100644
--- a/rss.go
+++ b/rss.go
@@ -17,12 +17,14 @@ import (
"time"
)
+// The root Rss structure
type Rss struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
Channels []*Channel `xml:"channel"`
}
+// The Rss channel structure
type Channel struct {
Title string `xml:"title"`
Description string `xml:"description"`
@@ -33,12 +35,14 @@ type Channel struct {
Item []*Item `xml:"item"`
}
+// The rss image structure
type Image struct {
Url string `xml:"url"`
Title string `xml:"title"`
Link string `xml:"link"`
}
+// The Rss item structure
type Item struct {
Title string `xml:"title"`
Link string `xml:"link"`
@@ -49,6 +53,7 @@ type Item struct {
Image []*Image `xml:"image"`
}
+// Create a new RSS feed
func NewRss(title string, description string, link string) *Rss {
rss := &Rss{Version: "2.0",
Channels: []*Channel{
@@ -66,6 +71,7 @@ func NewRss(title string, description string, link string) *Rss {
return rss
}
+// Create a new, orphan Rss Item.
func NewRssItem(title, link, description, author, category string, pubTime time.Time) *Item {
return &Item{
Title: title,
@@ -78,7 +84,7 @@ func NewRssItem(title, link, description, author, category string, pubTime time.
}
}
-// Add a new Item to the feed
+// Add an Item to the feed, under this Channel
func (ch *Channel) AppendItem(i *Item) {
ch.Item = append(ch.Item, i)
}
diff --git a/server.go b/server.go
index f9dab72..a1e4559 100644
--- a/server.go
+++ b/server.go
@@ -11,10 +11,12 @@ import (
)
var (
+ // Favicon path and cache duration
faviconPath = filepath.Join(PublicDir, "favicon.ico")
faviconCache = 2 * 24 * time.Hour
)
+// Start serving the blog.
func run() {
h := handlers.FaviconHandler(
handlers.PanicHandler(
diff --git a/tpldata.go b/tpldata.go
index e4a6f3d..3a79d7c 100644
--- a/tpldata.go
+++ b/tpldata.go
@@ -3,8 +3,7 @@ package main
import (
"bufio"
"bytes"
- "log"
- "net/url"
+ "fmt"
"os"
"path/filepath"
"regexp"
@@ -14,6 +13,14 @@ import (
"github.com/russross/blackfriday"
)
+var (
+ ErrEmptyPost = fmt.Errorf("empty post file")
+ ErrInvalidFrontMatter = fmt.Errorf("invalid front matter")
+ ErrMissingFrontMatter = fmt.Errorf("missing front matter")
+)
+
+// The TemplateData structure contains all the relevant information passed to the
+// template to generate the static HTML file.
type TemplateData struct {
SiteName string
TagLine string
@@ -24,31 +31,25 @@ type TemplateData struct {
Next *ShortPost
}
+// Create a new TemplateData for the specified post.
func newTemplateData(p *LongPost, i int, r []*LongPost, all []*LongPost) *TemplateData {
- b, err := url.Parse(Options.BaseURL)
- if err != nil {
- panic(err) // TODO : Manage errors
- }
- u, err := b.Parse("/rss")
- if err != nil {
- panic(err)
- }
td := &TemplateData{
SiteName: Options.SiteName,
TagLine: Options.TagLine,
- RssURL: u.String(),
+ RssURL: RssURL,
Post: p,
Recent: r,
}
if i > 0 {
- td.Prev = all[i-1].Short()
+ td.Prev = all[i-1].ShortPost
}
if i < len(all)-1 {
- td.Next = all[i+1].Short()
+ td.Next = all[i+1].ShortPost
}
return td
}
+// The ShortPost structure defines the basic metadata of a post.
type ShortPost struct {
Slug string
Author string
@@ -59,18 +60,23 @@ type ShortPost struct {
ModTime time.Time
}
+// The LongPost structure adds the parsed content of the post to the embedded ShortPost information.
type LongPost struct {
*ShortPost
Content string
}
+// Replace special characters to form a valid slug (post path)
var rxSlug = regexp.MustCompile(`[^a-zA-Z\-_0-9]`)
+// Return a valid slug from the file name of the post.
func getSlug(fnm string) string {
return rxSlug.ReplaceAllString(strings.Replace(fnm, filepath.Ext(fnm), "", 1), "-")
}
-func readFrontMatter(s *bufio.Scanner) map[string]string {
+// Read the front matter from the post. If there is no front matter, this is
+// not a valid post.
+func readFrontMatter(s *bufio.Scanner) (map[string]string, error) {
m := make(map[string]string)
infm := false
for s.Scan() {
@@ -78,7 +84,7 @@ func readFrontMatter(s *bufio.Scanner) map[string]string {
if l == "---" { // The front matter is delimited by 3 dashes
if infm {
// This signals the end of the front matter
- return m
+ return m, nil
} else {
// This is the start of the front matter
infm = true
@@ -87,41 +93,39 @@ func readFrontMatter(s *bufio.Scanner) map[string]string {
sections := strings.SplitN(l, ":", 2)
if len(sections) != 2 {
// Invalid front matter line
- log.Println("POST ERROR invalid front matter line: ", l)
- return nil
+ return nil, ErrInvalidFrontMatter
}
m[sections[0]] = strings.Trim(sections[1], " ")
} else if l != "" {
// No front matter, quit
- return nil
+ return nil, ErrMissingFrontMatter
}
}
- if infm {
- log.Println("POST ERROR unclosed front matter")
- } else if err := s.Err(); err != nil {
- log.Println("POST ERROR ", err)
+ if err := s.Err(); err != nil {
+ return nil, err
}
- return nil
+ return nil, ErrEmptyPost
}
-func newLongPost(fi os.FileInfo) *LongPost {
- log.Println("processing post ", fi.Name())
+// Create a LongPost from the specified FileInfo.
+func newLongPost(fi os.FileInfo) (*LongPost, error) {
f, err := os.Open(filepath.Join(PostsDir, fi.Name()))
if err != nil {
- log.Println("POST ERROR ", err)
- return nil
+ return nil, err
}
defer f.Close()
s := bufio.NewScanner(f)
- m := readFrontMatter(s)
+ m, err := readFrontMatter(s)
+ if err != nil {
+ return nil, err
+ }
slug := getSlug(fi.Name())
pubdt := fi.ModTime()
if dt, ok := m["Date"]; ok {
pubdt, err = time.Parse("2006-01-02", dt)
if err != nil {
- log.Println("POST ERROR invalid date format: ", dt)
- pubdt = fi.ModTime()
+ return nil, err
}
}
sp := &ShortPost{
@@ -140,17 +144,12 @@ func newLongPost(fi os.FileInfo) *LongPost {
buf.WriteString(s.Text() + "\n")
}
if err = s.Err(); err != nil {
- log.Println("POST ERROR ", err)
- return nil
+ return nil, err
}
res := blackfriday.MarkdownCommon(buf.Bytes())
lp := &LongPost{
sp,
string(res),
}
- return lp
-}
-
-func (lp *LongPost) Short() *ShortPost {
- return lp.ShortPost
+ return lp, nil
}
diff --git a/watch.go b/watch.go
index 6663614..7f9acba 100644
--- a/watch.go
+++ b/watch.go
@@ -2,6 +2,8 @@ package main
import (
"log"
+ "path/filepath"
+ "strings"
"time"
"github.com/howeyc/fsnotify"
@@ -14,6 +16,7 @@ const (
watchEventDelay = 10 * time.Second
)
+// Create and start a watcher, watching both the posts and the templates directories.
func startWatcher() *fsnotify.Watcher {
w, err := fsnotify.NewWatcher()
if err != nil {
@@ -33,7 +36,7 @@ func startWatcher() *fsnotify.Watcher {
return w
}
-// Receive watcher events for the posts directory. All events require re-generating
+// Receive watcher events for the directories. All events require re-generating
// the whole site (because the template may display the n most recent posts, the
// next and previous post, etc.). It could be fine-tuned based on what data we give
// to the templates, but for now, lazy approach.
@@ -41,10 +44,17 @@ func watch(w *fsnotify.Watcher) {
var delay <-chan time.Time
for {
select {
- case <-w.Event:
+ case ev := <-w.Event:
// Regenerate the files after the delay, reset the delay if an event is triggered
// in the meantime
- delay = time.After(watchEventDelay)
+ ext := filepath.Ext(ev.Name)
+ // Care only about changes to markdown files in the Posts directory, or to
+ // Amber or Native Go template files in the Templates directory.
+ if strings.HasPrefix(ev.Name, PostsDir) && ext == ".md" {
+ delay = time.After(watchEventDelay)
+ } else if strings.HasPrefix(ev.Name, TemplatesDir) && (ext == ".amber" || ext == ".html") {
+ delay = time.After(watchEventDelay)
+ }
case err := <-w.Error:
log.Println("WATCH ERROR ", err)