As an exercise, I wrote an opinionated simple static site generator in Rust. I like to use Gulp and Bower for front-end development, so this tool is responsible only for converting the markdown posts and pages into HTML and providing some global variables. Gulp handles everything else (including the development server).

#![feature(custom_derive, plugin)]
#![plugin(serde_macros)]

extern crate serde;
extern crate serde_json;
extern crate serde_yaml;
extern crate handlebars;
extern crate pulldown_cmark;
extern crate glob;
extern crate walkdir;
extern crate notify;

#[macro_use] extern crate maplit;
#[macro_use] extern crate log;

pub mod err {
    
    macro_rules! from {
        ($t: ty) => {
            impl ::std::convert::From<$t> for Error {
                fn from(e: $t) -> Self {
                    Error(e.into())
                }
            }
        }
    }

    #[derive(Debug)]
    pub struct Error(pub Box<::std::error::Error + Send + Sync>);

    impl ::std::error::Error for Error {
        fn description(&self) -> &str {
            self.0.description()
        }
    }

    impl ::std::fmt::Display for Error {
        fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
            ::std::error::Error::description(self).fmt(f)
        }
    }

    from!(&'static str);
    from!(String);
    from!(::std::io::Error);
    from!(::serde_yaml::Error);
    from!(::handlebars::TemplateError);
    from!(::handlebars::TemplateFileError);
    from!(::handlebars::RenderError);

    pub type Result<T> = ::std::result::Result<T, Error>;

}

use ::std::rc::Rc;
use ::std::collections::BTreeMap;

fn md_to_html(md: &str) -> String {
    let mut s = String::new();
    let p = ::pulldown_cmark::Parser::new(md);
    ::pulldown_cmark::html::push_html(&mut s, p);
    s
}

fn parse_excerpt<S>(value: &Option<String>, ser: &mut S) -> Result<(), S::Error> 
    where S: ::serde::Serializer {
    use ::serde::Serialize;
    Ok(try!(value.as_ref().map(|x| md_to_html(&x) ).serialize(ser)))
}



#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Metadata {
    name: Option<String>,
    path: String,
    layout: String,
    title: Option<String>,
    date: Option<String>,
    #[serde(serialize_with="parse_excerpt")]
    excerpt: Option<String>,
    #[serde(default)]
    tags: Vec<String>,
    other: Option<::serde_json::Value>,
    #[serde(skip_deserializing)]
    common: Option<Rc<Common>>,
}

impl Metadata {
    fn name(&self) -> &str { self.name.as_ref().unwrap() }
}

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Common {
    title: String,
    author: String,
    #[serde(default)]
    posts: Vec<Metadata>,
    #[serde(default)]
    pages: Vec<Metadata>,
    other: Option<::serde_json::Value>,
    #[serde(default)]
    tags: BTreeMap<String, Vec<Metadata>>
}


pub fn markdown_helper(
    c: &::handlebars::Context, 
    h: &::handlebars::Helper, 
    r : &::handlebars::Handlebars, 
    rc: &mut ::handlebars::RenderContext) -> Result<(), ::handlebars::RenderError> {
    
    use ::handlebars::Renderable;

    if let Some(cont) = h.template().map(|x| x.to_string() ) {
        let s = md_to_html(&cont);

        if let Ok(t) = ::handlebars::Template::compile(&s) {
            return t.render(c, r, rc);
        }
    }

    Err(::handlebars::RenderError::new("Wrong arguments for \"markdown\""))
}

pub fn process(src: &str, dest: &str) -> err::Result<()> {

    use ::std::path::Path;
    use ::std::fs::File;
    use ::std::io::{Read, Write};
    use ::walkdir::WalkDir;

    fn extract(path: &Path) -> err::Result<(Metadata, String)> {

        let mut buff = String::new();
        let mut fo = try!(File::open(path));
        let _ = try!(fo.read_to_string(&mut buff));

        let mut spl = buff.split("---");

        let _ = try!(spl.next().ok_or("Wrong file metadata"));
        let md = try!(spl.next().ok_or("Wrong file metadata"));
        let cont = try!(spl.next().ok_or("Wrong file metadata"));

        let mut md: Metadata = try!(::serde_yaml::from_str(&md));

        if md.name.is_none() {
            let name = try!(path.file_stem()
                .and_then(|x| x.to_str())
                .ok_or("Can't determine file name"));
            md.name = Some(name.into());
        }

        Ok((md, cont.to_owned()))
    }

    let src_path = Path::new(src);
    let dest_path = Path::new(dest);

    let mut common: Common = {
        let f = try!(File::open(src_path.join("_config.yml")));
        try!(::serde_yaml::from_reader(f))
    };

    let mut handlebars = {
        let mut handlebars = ::handlebars::Handlebars::new();
        handlebars.register_helper("markdown", Box::new(markdown_helper));
        handlebars
    };

    for fp in WalkDir::new(src_path.join("_posts"))
        .min_depth(1)
        .into_iter()
        .flat_map(|x| x) {

        let (md, cont) = try!(extract(&fp.path()));

        for tag in &md.tags {
            common.tags.entry(tag.clone())
                .or_insert_with(Vec::new)
                .push(md.clone());
        }

        let _ = try!(handlebars.register_template_string(&md.name(), cont));

        common.posts.push(md);
    }

    let _ = common.posts.sort_by(|a, b| b.date.cmp(&a.date));

    for fp in WalkDir::new(src_path.join("_pages"))
        .min_depth(1)
        .into_iter()
        .flat_map(|x| x) {

        let (md, cont) = try!(extract(&fp.path()));

        let _ = try!(handlebars.register_template_string(&md.name(), cont));

        common.pages.push(md);
    }

    for fp in WalkDir::new(src_path.join("_layouts"))
        .min_depth(1)
        .into_iter()
        .flat_map(|x| x) {

        let ref name = try!(fp.path().file_stem()
            .and_then(|x| x.to_str())
            .ok_or("Can't determine layout name"));

        let _ = try!(handlebars.register_template_file(name, &fp.path()));
    }

    let common = Rc::new(common);

    for md in common.posts.iter().chain(common.pages.iter()) {
        let mut fo = {
            let path = if md.path.ends_with("/") {
                format!(".{}index.html", &md.path) 
            } else {
                md.path.clone()
            };

            let path = dest_path.join(&path);

            if let Some(parent) = path.parent() {
                let _ = try!(::std::fs::create_dir_all(parent));
            }

            try!(File::create(path))
        };
        let ctx = {
            let md = Metadata { common: Some(common.clone()), ..md.clone() };
            ::handlebars::Context::wraps(&md)
        };
        let _ = try!(handlebars.renderw(&md.name(), &ctx, &mut fo));
    }

    Ok(())
}

pub fn watch(src: &str, dest: &str) -> err::Result<()> {
    use notify::{RecommendedWatcher, Error, Watcher};
    use std::sync::mpsc::channel;

    let (tx, rx) = channel();
    let mut watcher: RecommendedWatcher = try!(Watcher::new(tx)
        .map_err(|_| "Notify error"));

    let _ = try!(watcher.watch(src)
        .map_err(|_| "Notify error"));

    loop {
        if let Ok(val) = rx.recv() {
            info!("file changed {:?}", val);
            if let Err(e) = process(src, dest) {
                info!("Possible processing error");
            }
        }
    }
}