Skip to content

Commit 3c44684

Browse files
authored
feat: RSS feeds (#137)
fix #8
1 parent d782a74 commit 3c44684

File tree

6 files changed

+179
-4
lines changed

6 files changed

+179
-4
lines changed

Cargo.lock

Lines changed: 61 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ url = "2.5.3"
3030
rust-embed = { version = "8.5.0", features = ["interpolate-folder-path"] }
3131
lazy_static = "1.5.0"
3232
indexmap = { version = "2.6.0", features = ["serde"] }
33+
rss = "2.0.9"
3334

3435
[profile.release]
3536
codegen-units = 1

example/templates/base.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<link rel="icon" type="image/x-icon" href="./static/favicon.ico">
77
<meta name="viewport" content="width=device-width, initial-scale=1">
88
<meta name="color-scheme" content="light dark" />
9+
<meta name="generator" content="Marmite" />
910
{% block seo %}
1011
<meta property="og:title" content="{{ site.name }}">
1112
<meta property="og:description" content="{{ site.tagline }}">
@@ -24,6 +25,22 @@
2425
<link rel="stylesheet" type="text/css" href="./static/marmite.css">
2526
<link rel="stylesheet" type="text/css" href="./static/custom.css">
2627
{% endblock -%}
28+
{%- block feeds %}
29+
<link rel="alternate" type="application/rss+xml" title="index" href="index.rss">
30+
{% for stream, _ in group(kind="stream") -%}
31+
<link rel="alternate" type="application/rss+xml" title="{{stream}}" href="{{stream | slugify}}.rss">
32+
{% endfor %}
33+
{%- for tag, _ in group(kind="tag") -%}
34+
<link rel="alternate" type="application/rss+xml" title="{{tag}}" href="tag-{{tag | slugify}}.rss">
35+
{% endfor %}
36+
{%- for author, _ in group(kind="author") -%}
37+
<link rel="alternate" type="application/rss+xml" title="{{author}}" href="author-{{author | slugify}}.rss">
38+
{% endfor %}
39+
{%- for year, _ in group(kind="archive") -%}
40+
<link rel="alternate" type="application/rss+xml" title="{{year}}" href="archive-{{year}}.rss">
41+
{% endfor %}
42+
43+
{% endblock %}
2744
</head>
2845
<body>
2946
<main class="container">

src/feed.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use rss::{ChannelBuilder, ItemBuilder};
2+
use std::fs::File;
3+
use std::io::prelude::*;
4+
use std::path::Path;
5+
6+
use crate::config::Marmite;
7+
use crate::content::Content;
8+
9+
pub fn generate_rss(
10+
contents: &[Content],
11+
output_path: &Path,
12+
filename: &str,
13+
config: &Marmite,
14+
) -> Result<(), String> {
15+
let mut channel = ChannelBuilder::default()
16+
.title(&config.name)
17+
.link(&config.url)
18+
.description(&config.tagline)
19+
.generator("marmite".to_string())
20+
.build();
21+
22+
for content in contents.iter().take(15) {
23+
let mut item = ItemBuilder::default()
24+
.title(content.title.clone())
25+
.link(format!("{}/{}", &config.url, &content.slug))
26+
.guid(rss::GuidBuilder::default().value(&content.slug).build())
27+
.pub_date(content.date.unwrap().to_string())
28+
.content(content.html.clone())
29+
.source(
30+
rss::SourceBuilder::default()
31+
.url(&config.url)
32+
.title(filename.to_string())
33+
.build(),
34+
)
35+
.description(content.description.clone())
36+
.build();
37+
38+
if let Some(author) = content.authors.first() {
39+
item.author = Some(author.clone());
40+
}
41+
item.categories = content
42+
.tags
43+
.iter()
44+
.map(|tag| rss::CategoryBuilder::default().name(tag.clone()).build())
45+
.collect();
46+
channel.items.push(item);
47+
}
48+
49+
if let Some(latest_item) = channel.items.first() {
50+
channel.pub_date = latest_item.pub_date.clone();
51+
}
52+
53+
channel.last_build_date = Some(chrono::Utc::now().format("%+").to_string());
54+
55+
if !config.card_image.is_empty() {
56+
channel.image = Some(
57+
rss::ImageBuilder::default()
58+
.url(format!("{}/{}", &config.url, &config.card_image))
59+
.build(),
60+
);
61+
}
62+
63+
let rss = channel.to_string();
64+
let feed_path = output_path.join(format!("{filename}.rss"));
65+
let mut file = File::create(feed_path).map_err(|e| e.to_string())?;
66+
file.write_all(rss.as_bytes()).map_err(|e| e.to_string())?;
67+
68+
Ok(())
69+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod cli;
77
mod config;
88
mod content;
99
mod embedded;
10+
mod feed;
1011
mod markdown;
1112
mod server;
1213
mod site;

src/site.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ fn render_templates(
319319
output_dir,
320320
&stream_slug,
321321
)?;
322+
// Render {stream}.rss for each stream
323+
crate::feed::generate_rss(&stream_contents, output_dir, &stream_slug, &site_data.site)?;
322324
}
323325

324326
// Pages are treated as a list of content, no stream separation is needed
@@ -428,14 +430,23 @@ fn handle_author_pages(
428430
.cloned()
429431
.collect::<Vec<Content>>();
430432

433+
let filename = format!("author-{}", &author_slug);
431434
handle_list_page(
432435
&author_context,
433436
&author.name,
434437
&author_posts,
435438
site_data,
436439
tera,
437440
output_dir,
438-
format!("author-{}", &author_slug).as_ref(),
441+
&filename,
442+
)?;
443+
444+
// Render author-{name}.rss for each stream
445+
crate::feed::generate_rss(
446+
&author_posts,
447+
output_dir,
448+
&filename.clone(),
449+
&site_data.site,
439450
)?;
440451
}
441452

@@ -778,14 +789,22 @@ fn handle_tag_pages(
778789
) -> Result<(), String> {
779790
for (tag, tagged_contents) in site_data.tag.iter() {
780791
let tag_slug = slugify(tag);
792+
let filename = format!("tag-{}", &tag_slug);
781793
handle_list_page(
782794
global_context,
783795
&site_data.site.tags_content_title.replace("$tag", tag),
784796
&tagged_contents,
785797
site_data,
786798
tera,
787799
output_dir,
788-
format!("tag-{}", &tag_slug).as_ref(),
800+
&filename,
801+
)?;
802+
// Render tag-{tag}.rss for each stream
803+
crate::feed::generate_rss(
804+
&tagged_contents,
805+
output_dir,
806+
&filename.clone(),
807+
&site_data.site,
789808
)?;
790809
}
791810

@@ -811,14 +830,22 @@ fn handle_archive_pages(
811830
tera: &Tera,
812831
) -> Result<(), String> {
813832
for (year, archive_contents) in site_data.archive.iter() {
833+
let filename = format!("archive-{year}");
814834
handle_list_page(
815835
global_context,
816836
&site_data.site.archives_content_title.replace("$year", year),
817837
&archive_contents,
818838
site_data,
819839
tera,
820840
output_dir,
821-
format!("archive-{year}").as_ref(),
841+
&filename,
842+
)?;
843+
// Render archive-{year}.rss for each stream
844+
crate::feed::generate_rss(
845+
&archive_contents,
846+
output_dir,
847+
&filename.clone(),
848+
&site_data.site,
822849
)?;
823850
}
824851

0 commit comments

Comments
 (0)