Skip to content

Commit bf61a8b

Browse files
committed
add support for multiple blogs
1 parent 66af6fb commit bf61a8b

File tree

7 files changed

+219
-116
lines changed

7 files changed

+219
-116
lines changed

posts/blog.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
title: Rust Blog
2+
index-title: The Rust Programming Language Blog
3+
description: Empowering everyone to build reliable and efficient software.
4+
maintained-by: the Rust Team

src/blogs.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use crate::posts::Post;
2+
use serde_derive::{Deserialize, Serialize};
3+
use std::error::Error;
4+
use std::path::{Path, PathBuf};
5+
6+
static MANIFEST_FILE: &str = "blog.yml";
7+
static POSTS_EXT: &str = "md";
8+
9+
#[derive(Deserialize)]
10+
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
11+
struct Manifest {
12+
title: String,
13+
index_title: String,
14+
description: String,
15+
maintained_by: String,
16+
}
17+
18+
#[derive(Serialize)]
19+
pub(crate) struct Blog {
20+
title: String,
21+
index_title: String,
22+
description: String,
23+
maintained_by: String,
24+
#[serde(serialize_with = "add_postfix_slash")]
25+
prefix: PathBuf,
26+
posts: Vec<Post>,
27+
}
28+
29+
impl Blog {
30+
fn load(prefix: PathBuf, dir: &Path) -> Result<Self, Box<dyn Error>> {
31+
let manifest_content = std::fs::read_to_string(dir.join(MANIFEST_FILE))?;
32+
let manifest: Manifest = serde_yaml::from_str(&manifest_content)?;
33+
34+
let mut posts = Vec::new();
35+
for entry in std::fs::read_dir(dir)? {
36+
let path = entry?.path();
37+
let ext = path.extension().and_then(|e| e.to_str());
38+
if path.metadata()?.file_type().is_file() && ext == Some(POSTS_EXT) {
39+
posts.push(Post::open(&path)?);
40+
}
41+
}
42+
43+
posts.sort_by_key(|post| post.url.clone());
44+
posts.reverse();
45+
46+
// Decide which posts should show the year in the index.
47+
posts[0].show_year = true;
48+
for i in 1..posts.len() {
49+
posts[i].show_year = posts[i - 1].year != posts[i].year;
50+
}
51+
52+
Ok(Blog {
53+
title: manifest.title,
54+
index_title: manifest.index_title,
55+
description: manifest.description,
56+
maintained_by: manifest.maintained_by,
57+
prefix,
58+
posts,
59+
})
60+
}
61+
62+
pub(crate) fn title(&self) -> &str {
63+
&self.title
64+
}
65+
66+
pub(crate) fn index_title(&self) -> &str {
67+
&self.index_title
68+
}
69+
70+
pub(crate) fn prefix(&self) -> &Path {
71+
&self.prefix
72+
}
73+
74+
pub(crate) fn posts(&self) -> &[Post] {
75+
&self.posts
76+
}
77+
}
78+
79+
/// Recursively load blogs in a directory. A blog is a directory with a `blog.yml`
80+
/// file inside it.
81+
pub(crate) fn load(base: &Path) -> Result<Vec<Blog>, Box<dyn Error>> {
82+
let mut blogs = Vec::new();
83+
load_recursive(base, base, &mut blogs)?;
84+
Ok(blogs)
85+
}
86+
87+
fn load_recursive(
88+
base: &Path,
89+
current: &Path,
90+
blogs: &mut Vec<Blog>,
91+
) -> Result<(), Box<dyn Error>> {
92+
for entry in std::fs::read_dir(current)? {
93+
let path = entry?.path();
94+
let file_type = path.metadata()?.file_type();
95+
96+
if file_type.is_dir() {
97+
load_recursive(base, &path, blogs)?;
98+
} else if file_type.is_file() {
99+
let file_name = path.file_name().and_then(|n| n.to_str());
100+
if let (Some(file_name), Some(parent)) = (file_name, path.parent()) {
101+
if file_name == MANIFEST_FILE {
102+
let prefix = parent
103+
.strip_prefix(base)
104+
.map(|p| p.to_path_buf())
105+
.unwrap_or_else(|_| PathBuf::new());
106+
blogs.push(Blog::load(prefix, parent)?);
107+
}
108+
}
109+
}
110+
}
111+
Ok(())
112+
}
113+
114+
fn add_postfix_slash<S>(path: &PathBuf, serializer: S) -> Result<S::Ok, S::Error>
115+
where
116+
S: serde::Serializer,
117+
{
118+
let mut str_repr = path.to_string_lossy().to_string();
119+
if !str_repr.is_empty() {
120+
str_repr.push('/');
121+
}
122+
serializer.serialize_str(&str_repr)
123+
}

src/main.rs

Lines changed: 71 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1+
mod blogs;
12
mod posts;
23

4+
use crate::blogs::Blog;
35
use crate::posts::Post;
46
use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError};
57
use sass_rs::{compile_file, Options};
68
use serde_derive::Serialize;
79
use serde_json::json;
10+
use std::convert::AsRef;
811
use std::error::Error;
912
use std::fs::{self, File};
1013
use std::io::Write;
11-
use std::path::PathBuf;
14+
use std::path::{Path, PathBuf};
1215

13-
struct Blog {
16+
struct Generator {
1417
handlebars: Handlebars,
15-
posts: Vec<Post>,
18+
blogs: Vec<Blog>,
1619
out_directory: PathBuf,
1720
}
1821

@@ -62,73 +65,34 @@ fn hb_month_helper<'a>(
6265
Ok(())
6366
}
6467

65-
impl Blog {
66-
fn new<T>(out_directory: T, posts_directory: T) -> Result<Blog, Box<dyn Error>>
67-
where
68-
T: Into<PathBuf>,
69-
{
68+
impl Generator {
69+
fn new(
70+
out_directory: impl AsRef<Path>,
71+
posts_directory: impl AsRef<Path>,
72+
) -> Result<Generator, Box<dyn Error>> {
7073
let mut handlebars = Handlebars::new();
71-
7274
handlebars.set_strict_mode(true);
73-
7475
handlebars.register_templates_directory(".hbs", "templates")?;
75-
7676
handlebars.register_helper("month_name", Box::new(hb_month_helper));
7777

78-
let posts = Blog::load_posts(posts_directory.into())?;
79-
80-
Ok(Blog {
78+
Ok(Generator {
8179
handlebars,
82-
posts,
83-
out_directory: out_directory.into(),
80+
blogs: crate::blogs::load(posts_directory.as_ref())?,
81+
out_directory: out_directory.as_ref().into(),
8482
})
8583
}
8684

87-
fn load_posts(dir: PathBuf) -> Result<Vec<Post>, Box<dyn Error>> {
88-
let mut posts = Vec::new();
89-
90-
for entry in fs::read_dir(dir)? {
91-
let path = entry?.path();
92-
93-
// ignore vim temporary files
94-
let filename = path.file_name().unwrap().to_str().unwrap();
95-
if filename.starts_with(".") && filename.ends_with(".swp") {
96-
continue;
97-
}
98-
99-
posts.push(Post::open(&path)?);
100-
}
101-
102-
// finally, sort the posts. oldest first.
103-
posts.sort_by_key(|post| post.url.clone());
104-
posts.reverse();
105-
106-
for i in 1..posts.len() {
107-
posts[i].show_year = posts[i - 1].year != posts[i].year;
108-
}
109-
110-
Ok(posts)
111-
}
112-
11385
fn render(&self) -> Result<(), Box<dyn Error>> {
11486
// make sure our output directory exists
11587
fs::create_dir_all(&self.out_directory)?;
11688

117-
self.render_index()?;
118-
119-
self.render_posts()?;
120-
121-
self.render_feed()?;
122-
89+
for blog in &self.blogs {
90+
self.render_blog(blog)?;
91+
}
12392
self.compile_sass("app");
12493
self.compile_sass("fonts");
125-
12694
self.concat_vendor_css(vec!["skeleton", "tachyons"]);
127-
12895
self.copy_static_files()?;
129-
130-
self.generate_releases_feed()?;
131-
13296
Ok(())
13397
}
13498

@@ -154,70 +118,85 @@ impl Blog {
154118
fs::write("./static/styles/vendor.css", &concatted).expect("couldn't write vendor css");
155119
}
156120

157-
fn render_index(&self) -> Result<(), Box<dyn Error>> {
121+
fn render_blog(&self, blog: &Blog) -> Result<(), Box<dyn Error>> {
122+
std::fs::create_dir_all(self.out_directory.join(blog.prefix()))?;
123+
124+
self.render_index(blog)?;
125+
self.render_feed(blog)?;
126+
self.render_releases_feed(blog)?;
127+
for post in blog.posts() {
128+
self.render_post(blog, post)?;
129+
}
130+
Ok(())
131+
}
132+
133+
fn render_index(&self, blog: &Blog) -> Result<(), Box<dyn Error>> {
158134
let data = json!({
159-
"title": "The Rust Programming Language Blog",
135+
"title": blog.index_title(),
160136
"parent": "layout",
161-
"posts": self.posts,
137+
"blog": blog,
162138
});
163-
164-
self.render_template("index.html", "index", data)?;
165-
139+
self.render_template(blog.prefix().join("index.html"), "index", data)?;
166140
Ok(())
167141
}
168142

169-
fn render_posts(&self) -> Result<(), Box<dyn Error>> {
170-
for post in &self.posts {
171-
// first, we create the path
172-
//let path = PathBuf::from(&self.out_directory);
173-
174-
let path = PathBuf::from(&post.year);
175-
let path = path.join(&post.month);
176-
let path = path.join(&post.day);
177-
178-
fs::create_dir_all(self.out_directory.join(&path))?;
143+
fn render_post(&self, blog: &Blog, post: &Post) -> Result<(), Box<dyn Error>> {
144+
let path = blog
145+
.prefix()
146+
.join(&post.year)
147+
.join(&post.month)
148+
.join(&post.day);
149+
fs::create_dir_all(self.out_directory.join(&path))?;
179150

180-
// then, we render the page in that path
181-
let mut filename = PathBuf::from(&post.filename);
182-
filename.set_extension("html");
151+
// then, we render the page in that path
152+
let mut filename = PathBuf::from(&post.filename);
153+
filename.set_extension("html");
183154

184-
let data = json!({
185-
"title": format!("{} | Rust Blog", post.title),
186-
"parent": "layout",
187-
"post": post,
188-
});
189-
190-
self.render_template(path.join(filename).to_str().unwrap(), "post", data)?;
191-
}
155+
let data = json!({
156+
"title": format!("{} | {}", post.title, blog.title()),
157+
"parent": "layout",
158+
"blog": blog,
159+
"post": post,
160+
});
192161

162+
self.render_template(path.join(filename), "post", data)?;
193163
Ok(())
194164
}
195165

196-
fn render_feed(&self) -> Result<(), Box<dyn Error>> {
197-
let posts: Vec<_> = self.posts.iter().by_ref().take(10).collect();
198-
let data =
199-
json!({ "posts": posts, "feed_updated": time::now_utc().rfc3339().to_string() });
166+
fn render_feed(&self, blog: &Blog) -> Result<(), Box<dyn Error>> {
167+
let posts: Vec<_> = blog.posts().iter().take(10).collect();
168+
let data = json!({
169+
"blog": blog,
170+
"posts": posts,
171+
"feed_updated": time::now_utc().rfc3339().to_string(),
172+
});
200173

201-
self.render_template("feed.xml", "feed", data)?;
174+
self.render_template(blog.prefix().join("feed.xml"), "feed", data)?;
202175
Ok(())
203176
}
204177

205-
fn generate_releases_feed(&self) -> Result<(), Box<dyn Error>> {
206-
let posts = self.posts.clone();
178+
fn render_releases_feed(&self, blog: &Blog) -> Result<(), Box<dyn Error>> {
179+
let posts = blog.posts().iter().cloned().collect::<Vec<_>>();
207180
let is_released: Vec<&Post> = posts.iter().filter(|post| post.release).collect();
208181
let releases: Vec<ReleasePost> = is_released
209182
.iter()
210183
.map(|post| ReleasePost {
211184
title: post.title.clone(),
212-
url: post.url.clone(),
185+
url: blog
186+
.prefix()
187+
.join(post.url.clone())
188+
.to_string_lossy()
189+
.to_string(),
213190
})
214191
.collect();
215192
let data = Releases {
216193
releases: releases,
217194
feed_updated: time::now_utc().rfc3339().to_string(),
218195
};
219196
fs::write(
220-
self.out_directory.join("releases.json"),
197+
self.out_directory
198+
.join(blog.prefix())
199+
.join("releases.json"),
221200
serde_json::to_string(&data)?,
222201
)?;
223202
Ok(())
@@ -239,22 +218,19 @@ impl Blog {
239218

240219
fn render_template(
241220
&self,
242-
name: &str,
221+
name: impl AsRef<Path>,
243222
template: &str,
244223
data: serde_json::Value,
245224
) -> Result<(), Box<dyn Error>> {
246-
let out_file = self.out_directory.join(name);
247-
225+
let out_file = self.out_directory.join(name.as_ref());
248226
let file = File::create(out_file)?;
249-
250227
self.handlebars.render_to_write(template, &data, file)?;
251-
252228
Ok(())
253229
}
254230
}
255231

256232
fn main() -> Result<(), Box<dyn Error>> {
257-
let blog = Blog::new("site", "posts")?;
233+
let blog = Generator::new("site", "posts")?;
258234

259235
blog.render()?;
260236

0 commit comments

Comments
 (0)