Add to Cargo.toml
: libssg = "0.1.1"
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
- You will need to have
pandoc
installed to use Markdown. - Uses the handlebars template engine
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() > 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.