libssg

Add to Cargo.toml: libssg = "0.1.1"

  1. crates.io
  2. github

static site generation library

Build your own executable static generator that includes your building logic instead of using configuration files and command line arguments. Inspired by Hakyll

use libssg::*;
/*
 * $ tree
 * .
 * ├── Cargo.toml etc
 * ├── src
 * │   └── main.rs
 * ├── css
 * │   └── *.css
 * ├── images
 * │   └── *.png
 * ├── index.md
 * ├── posts
 * │   └── *.md
 * ├── _site
 * │   ├── css
 * │   │   └── *.css
 * │   ├── images
 * │   │   └── *.png
 * │   ├── index.html
 * │   ├── posts
 * │   │   └── *.html
 * │   └── rss.xml
 * └── templates
 *     ├── default.hbs
 *     └── post.hbs
*/

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut state = State::new()?;
    state
        .then(match_pattern(
            "^posts/*",
            Route::SetExtension("html"),
               Renderer::Pipeline(vec![
                   Renderer::LoadAndApplyTemplate("templates/post.hbs"),
                   Renderer::LoadAndApplyTemplate("templates/default.hbs"),
               ]),
            compiler_seq(
                pandoc(),
                Box::new(|state, path| {
                    let path = path
                        .strip_prefix(&state.output_dir().parent().unwrap())
                        .unwrap_or(&path)
                        .to_path_buf();
                    if state.verbosity() > 3 {
                        println!("adding {} to RSS snapshot", path.display());
                    }
                    let uuid = uuid_from_path(&path);
                    state.add_to_snapshot("main-rss-feed".into(), uuid);
                    Ok(Default::default())
                }),
            ),
        ))
        .then(match_pattern(
            "index.md",
            Route::SetExtension("html"),
            Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            pandoc(),
        ))
        .then(copy("^images/*", Route::Id))
        .then(copy("^css/*", Route::Id))
        .then(build_rss_feed(
            "rss.xml".into(),
            rss_feed(
                "main-rss-feed".into(),
                RssItem {
                    title: "example page".into(),
                    description: "example using libssg".into(),
                    link: "http://example.local".into(),
                    last_build_date: String::new(),
                    pub_date: "Thu, 01 Jan 1970 00:00:00 +0000".to_string(),
                    ttl: 1800,
                },
            ),
        ))
        .finish()?;
    Ok(())
}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <title>hello world!</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="./main.css" />
  </head>
  <body>
    <header>
      <h1>libssg</h1>
    </header>

    <main>
          {{ include body }}
    </main>


    <footer>
      <a rel="me" style="display: none;" href="https://mastodon/@user"></a>
    </footer>
  </body>
</html>
          
[package]
name = "site"
version = "0.1.0"
authors = ["user <user@user>"]
edition = "2018"

[dependencies]
libssg = { version "*" }
          

Generating RSS

        .then(libssg::match_pattern(
            "^posts/*",
            libssg::Route::SetExtension("html"),
            libssg::Renderer::Pipeline(vec![
                libssg::Renderer::LoadAndApplyTemplate("templates/post.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::compiler_seq(
                libssg::pandoc(),
                Box::new(|state, path| {
                    let path = path
                        .strip_prefix(&state.output_dir().parent().unwrap())
                        .unwrap_or(&path)
                        .to_path_buf();
                    if state.verbosity() &gt; 3 {
                        println!("adding {} to RSS snapshot", path.display());
                    }
                    let uuid = libssg::uuid_from_path(&path);
                    state.add_to_snapshot(RSS_FEED_SNAPSHOT.into(), uuid);
                    Ok(Default::default())
                }),
            ),
        ))
        .then(libssg::build_rss_feed(
            "rss.xml".into(),
            libssg::rss_feed(
                RSS_FEED_SNAPSHOT.into(),
                libssg::RssItem {
                    title: "libssg".into(),
                    description: "example".into(),
                    link: "http://localhost".into(),
                    last_build_date: String::new(),
                    pub_date: libssg::chrono::Local::now().to_rfc2822(),
                    ttl: 1800,
                },
            ),
        ))
                    

Generating post archives

    .then(libssg::create(
            "archive.html".into(),
            libssg::Renderer::Pipeline(vec![
                default_metadata(),
                libssg::Renderer::Custom(load_posts()),
                libssg::Renderer::LoadAndApplyTemplate("templates/post-list.hbs"),
                libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
            ]),
            libssg::pandoc(),
        ))

  fn load_posts() -> Box<dyn libssg::BFn> {
    Box::new(|state, context| {
        if !state.snapshots().contains_key(RSS_FEED_SNAPSHOT) {
            // No posts configured/found
            return Err(format!(r#"There are no snapshots with key `{}`, is the
            source rule empty (ie producing no items) or have you typed the
            name wrong?"#, &RSS_FEED_SNAPSHOT))?;
        }

        let snapshot = &state.snapshots()[RSS_FEED_SNAPSHOT];
        let mut items = Vec::with_capacity(snapshot.len());
        for artifact in snapshot.iter() {
            let mut item = state.artifacts()[&artifact].metadata.clone();
            item.insert(
                "url".into(),
                state.artifacts()[&artifact]
                    .path
                    .display()
                    .to_string()
                    .into(),
            );
            items.push(item);
        }
        items.sort_by(|a, b| {
            b["date_iso_8601"]
                .as_str()
                .unwrap()
                .cmp(&a["date_iso_8601"].as_str().unwrap())
        });
        context.insert("posts".into(), items.into());
        Ok(Default::default())
    })
}

                    

Generating a sitemap

templates/sitemap.hbs:

<ul>
{{ #each pages }}
<li><a href="{{ url }}">{{ title }}</a></li>
{{ /each }}
</ul>

-------------------
src/main.rs:

.then(libssg::create("sitemap.html".into(),
    libssg::Renderer::Pipeline(vec![
        default_metadata(),
        libssg::Renderer::LoadAndApplyTemplate("templates/sitemap.hbs"),
        libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
    ]),
libssg::sitemap(),
))
                    

Using linkify and cURL to check if all outgoing links can be reached.

    let mut state = libssg::State::new()?;
    state
    <....>
    .finish()?;

    if std::env::var("CHECK").is_ok() {
        use linkify::{LinkFinder, LinkKind};

        fn visit_dirs(dir: &Path) -> std::io::Result {
            let mut ret = true;
            if dir.is_dir() {
                for entry in std::fs::read_dir(dir)? {
                    let entry = entry?;
                    let path = entry.path();
                    if path.is_dir() {
                        ret = ret && visit_dirs(&path)?;
                    } else {
                        if path.extension() == Some(std::ffi::OsStr::new("html")) {
                            let mut file = std::fs::File::open(&path)?;
                            let finder = LinkFinder::new();
                            let mut r = String::new();
                            file.read_to_string(&mut r)?;
                            for link in finder.links(&r) {
                                if *link.kind() != LinkKind::Url {
                                    continue;
                                }
                                let out = Command::new("curl")
                                    .args(&["--fail", "-s", "-L", link.as_str()])
                                    .stdin(Stdio::null())
                                    .stdout(Stdio::null())
                                    .stderr(Stdio::null())
                                    .spawn()?
                                    .wait()?;
                                if !out.success() {
                                    ret = false;
                                    println!(
                                        "{}: Found dead/invalid outgoing link: {}",
                                        path.display(),
                                        link.as_str()
                                    );
                                }
                            }
                        }
                    }
                }
            }
            Ok(ret)
        }
        visit_dirs(state.output_dir())?;
    }

          

cargo run and the output is saved at ./_site/.

Set $FORCE, $VERBOSITY (0..5) to change behaviour.