RSS support
This commit is contained in:
1 parent
c79d5a479a
commit
aa06d2c5ef
14 files changed
+296
-58
No files matched your search
Generated
+139
@@ -46,6 +46,19 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atom_syndication"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21fb6a0b39c6517edafe46f8137e53c51742425a4dae1c73ee12264a37ad7541"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"derive_builder",
|
||||||
|
"diligent-date-parser",
|
||||||
|
"never",
|
||||||
|
"quick-xml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@@ -148,6 +161,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||||||
name = "blog_server"
|
name = "blog_server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atom_syndication",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"knuffel",
|
"knuffel",
|
||||||
@@ -156,6 +170,7 @@ dependencies = [
|
|||||||
"miette",
|
"miette",
|
||||||
"notify",
|
"notify",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
|
"rss",
|
||||||
"syntect",
|
"syntect",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -223,6 +238,90 @@ dependencies = [
|
|||||||
"syntect",
|
"syntect",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_core"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diligent-date-parser"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2d0fd95c7c02e2d6c588c6c5628466fff9bdde4b8c6196465e087b08e792720"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filetime"
|
name = "filetime"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -436,6 +535,12 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -742,6 +847,12 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "never"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "4.0.17"
|
version = "4.0.17"
|
||||||
@@ -967,6 +1078,16 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -1002,6 +1123,18 @@ version = "0.6.25"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rss"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acaf1331b7fc4edc3c2920819fee1766c27e8d40da593155832db3d6dea64e92"
|
||||||
|
dependencies = [
|
||||||
|
"atom_syndication",
|
||||||
|
"derive_builder",
|
||||||
|
"never",
|
||||||
|
"quick-xml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
@@ -1121,6 +1254,12 @@ dependencies = [
|
|||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "supports-color"
|
name = "supports-color"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ tower = { version = "0.4", features = ["limit"] }
|
|||||||
tower-http = { version = "0.3", features = ["fs", "trace"] }
|
tower-http = { version = "0.3", features = ["fs", "trace"] }
|
||||||
# Compile-time HTTP templating
|
# Compile-time HTTP templating
|
||||||
maud = "0.23"
|
maud = "0.23"
|
||||||
|
# Serialisation for RSS and Atom
|
||||||
|
atom_syndication = "0.11"
|
||||||
|
rss = "2"
|
||||||
# KDL parsing
|
# KDL parsing
|
||||||
knuffel = "2"
|
knuffel = "2"
|
||||||
# CommonMark parsing
|
# CommonMark parsing
|
||||||
|
|||||||
+26
-10
@@ -16,16 +16,32 @@ use codeblock::CodeBlockRenderer;
|
|||||||
use posts_store::ConcurrentPostsStore;
|
use posts_store::ConcurrentPostsStore;
|
||||||
use render::Renderer;
|
use render::Renderer;
|
||||||
|
|
||||||
#[derive(knuffel::Decode)]
|
#[derive(knuffel::Decode, Clone, Debug)]
|
||||||
struct Config {
|
pub struct Config {
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child, unwrap(argument))]
|
||||||
bind: String,
|
bind: String,
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
concurrency_limit: usize,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
posts_dir: PathBuf,
|
posts_dir: PathBuf,
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child, unwrap(argument))]
|
||||||
static_dir: PathBuf,
|
static_dir: PathBuf,
|
||||||
|
#[knuffel(child)]
|
||||||
|
rss: RssConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(knuffel::Decode, Clone, Debug)]
|
||||||
|
pub struct RssConfig {
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child, unwrap(argument))]
|
||||||
concurrency_limit: usize,
|
num_posts: usize,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
title: String,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
ttl: u32,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
protocol: String,
|
||||||
|
#[knuffel(child, unwrap(argument))]
|
||||||
|
domain: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> miette::Result<()> {
|
fn main() -> miette::Result<()> {
|
||||||
@@ -79,18 +95,18 @@ fn main() -> miette::Result<()> {
|
|||||||
.block_on(run(config, posts_store))
|
.block_on(run(config, posts_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(config: Config, posts_store: ConcurrentPostsStore) -> miette::Result<()> {
|
async fn run(
|
||||||
let service = service::site_service(
|
config: Config,
|
||||||
posts_store,
|
posts_store: ConcurrentPostsStore,
|
||||||
&config.static_dir,
|
) -> miette::Result<()>
|
||||||
config.concurrency_limit
|
{
|
||||||
);
|
|
||||||
|
|
||||||
let bind_address = &config.bind
|
let bind_address = &config.bind
|
||||||
.parse()
|
.parse()
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
.wrap_err_with(|| format!("Failed to parse socket address \"{}\"", config.bind))?;
|
.wrap_err_with(|| format!("Failed to parse socket address \"{}\"", config.bind))?;
|
||||||
|
|
||||||
|
let service = service::site_service(config, posts_store);
|
||||||
|
|
||||||
info!(address = %bind_address, "Starting server");
|
info!(address = %bind_address, "Starting server");
|
||||||
|
|
||||||
Server::try_bind(bind_address)
|
Server::try_bind(bind_address)
|
||||||
|
|||||||
@@ -125,12 +125,12 @@ impl Post {
|
|||||||
) -> Result<Self, ParseError>
|
) -> Result<Self, ParseError>
|
||||||
{
|
{
|
||||||
let mdpost = MdPost::parse(file_name, source)?;
|
let mdpost = MdPost::parse(file_name, source)?;
|
||||||
Ok(Self::from_mdpost(post_id, code_renderer, created, updated, mdpost))
|
Ok(Self::from_mdpost(code_renderer, post_id, created, updated, mdpost))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_mdpost(
|
fn from_mdpost(
|
||||||
id: PostId,
|
|
||||||
code_renderer: &CodeBlockRenderer,
|
code_renderer: &CodeBlockRenderer,
|
||||||
|
id: PostId,
|
||||||
created: DateTime<Utc>,
|
created: DateTime<Utc>,
|
||||||
updated: DateTime<Utc>,
|
updated: DateTime<Utc>,
|
||||||
mdpost: MdPost,
|
mdpost: MdPost,
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ use chrono::{DateTime, Utc};
|
|||||||
use notify::DebouncedEvent;
|
use notify::DebouncedEvent;
|
||||||
use tracing::{info, warn, error};
|
use tracing::{info, warn, error};
|
||||||
|
|
||||||
use crate::codeblock::CodeBlockRenderer;
|
use crate::{
|
||||||
use crate::post::{ParseError, Post, PostId};
|
codeblock::CodeBlockRenderer,
|
||||||
use crate::posts_store::ConcurrentPostsStore;
|
post::{ParseError, Post, PostId},
|
||||||
|
posts_store::ConcurrentPostsStore,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
posts: ConcurrentPostsStore,
|
posts: ConcurrentPostsStore,
|
||||||
@@ -125,7 +127,8 @@ impl Renderer {
|
|||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
fn remove(&self, target: &EventTarget) {
|
fn remove(&self, target: &EventTarget) {
|
||||||
self.posts.write_blocking().remove(&target.id);
|
let mut guard = self.posts.write_blocking();
|
||||||
|
guard.remove(&target.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use maud::html;
|
use maud::html;
|
||||||
|
|
||||||
use crate::template;
|
use crate::template;
|
||||||
use super::response::HtmlResponse;
|
use super::response::Html;
|
||||||
|
|
||||||
pub async fn handle() -> HtmlResponse {
|
pub async fn handle() -> Html {
|
||||||
HtmlResponse::new()
|
Html::new()
|
||||||
.with_title_static("Contact")
|
.with_title_static("Contact")
|
||||||
.with_crawler_permissive()
|
.with_crawler_permissive()
|
||||||
.with_head(html! {
|
.with_head(html! {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ use crate::{
|
|||||||
posts_store::ConcurrentPostsStore,
|
posts_store::ConcurrentPostsStore,
|
||||||
template,
|
template,
|
||||||
};
|
};
|
||||||
use super::response::HtmlResponse;
|
use super::response::Html;
|
||||||
|
|
||||||
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> HtmlResponse {
|
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
|
||||||
HtmlResponse::new()
|
Html::new()
|
||||||
.with_title_static("Pantonshire")
|
.with_title_static("Pantonshire")
|
||||||
.with_crawler_permissive()
|
.with_crawler_permissive()
|
||||||
.with_head(html! {
|
.with_head(html! {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ mod index;
|
|||||||
mod post;
|
mod post;
|
||||||
mod posts_list;
|
mod posts_list;
|
||||||
mod response;
|
mod response;
|
||||||
|
mod rss;
|
||||||
mod site;
|
mod site;
|
||||||
mod static_content;
|
mod static_content;
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ use axum::extract::{Extension, Path};
|
|||||||
use maud::html;
|
use maud::html;
|
||||||
|
|
||||||
use crate::posts_store::ConcurrentPostsStore;
|
use crate::posts_store::ConcurrentPostsStore;
|
||||||
use super::response::{ErrorResponse, HtmlResponse};
|
use super::response::{Error, Html};
|
||||||
|
|
||||||
pub async fn handle(
|
pub async fn handle(
|
||||||
Path(post_id): Path<String>,
|
Path(post_id): Path<String>,
|
||||||
Extension(posts): Extension<ConcurrentPostsStore>
|
Extension(posts): Extension<ConcurrentPostsStore>
|
||||||
) -> Result<HtmlResponse, ErrorResponse>
|
) -> Result<Html, Error>
|
||||||
{
|
{
|
||||||
let post = posts.get(&post_id)
|
let post = posts.get(&post_id)
|
||||||
.await
|
.await
|
||||||
.ok_or(ErrorResponse::PostNotFound)?;
|
.ok_or(Error::PostNotFound)?;
|
||||||
|
|
||||||
Ok(HtmlResponse::new()
|
Ok(Html::new()
|
||||||
.with_crawler_permissive()
|
.with_crawler_permissive()
|
||||||
.with_title_owned(post.title().to_owned())
|
.with_title_owned(post.title().to_owned())
|
||||||
.with_head(html! {
|
.with_head(html! {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ use crate::{
|
|||||||
posts_store::ConcurrentPostsStore,
|
posts_store::ConcurrentPostsStore,
|
||||||
template,
|
template,
|
||||||
};
|
};
|
||||||
use super::response::HtmlResponse;
|
use super::response::Html;
|
||||||
|
|
||||||
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> HtmlResponse {
|
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
|
||||||
HtmlResponse::new()
|
Html::new()
|
||||||
.with_title_static("Articles")
|
.with_title_static("Articles")
|
||||||
.with_crawler_permissive()
|
.with_crawler_permissive()
|
||||||
.with_head(html! {
|
.with_head(html! {
|
||||||
|
|||||||
@@ -4,31 +4,32 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
http::StatusCode,
|
body::{Bytes, Full},
|
||||||
response::{IntoResponse, Html, Response},
|
http::{header::{self, HeaderValue}, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use maud::{html, Markup, Render, Escaper, DOCTYPE};
|
use maud::{html, Markup, Render, Escaper, DOCTYPE};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ErrorResponse {
|
pub enum Error {
|
||||||
Internal,
|
Internal,
|
||||||
PostNotFound,
|
PostNotFound,
|
||||||
StaticResourceNotFound,
|
StaticResourceNotFound,
|
||||||
RouteNotFound,
|
RouteNotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorResponse {
|
impl Error {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
ErrorResponse::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
Error::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ErrorResponse::PostNotFound => StatusCode::NOT_FOUND,
|
Error::PostNotFound => StatusCode::NOT_FOUND,
|
||||||
ErrorResponse::StaticResourceNotFound => StatusCode::NOT_FOUND,
|
Error::StaticResourceNotFound => StatusCode::NOT_FOUND,
|
||||||
ErrorResponse::RouteNotFound => StatusCode::NOT_FOUND,
|
Error::RouteNotFound => StatusCode::NOT_FOUND,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for ErrorResponse {
|
impl IntoResponse for Error {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let status_code = self.status_code();
|
let status_code = self.status_code();
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ impl IntoResponse for ErrorResponse {
|
|||||||
buf
|
buf
|
||||||
};
|
};
|
||||||
|
|
||||||
HtmlResponse::new()
|
Html::new()
|
||||||
.with_status(status_code)
|
.with_status(status_code)
|
||||||
.with_body(html! {
|
.with_body(html! {
|
||||||
p { (status_text) }
|
p { (status_text) }
|
||||||
@@ -61,7 +62,7 @@ impl IntoResponse for ErrorResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HtmlResponse {
|
pub struct Html {
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
title: Cow<'static, str>,
|
title: Cow<'static, str>,
|
||||||
head: Option<Markup>,
|
head: Option<Markup>,
|
||||||
@@ -69,7 +70,7 @@ pub struct HtmlResponse {
|
|||||||
crawler_hints: CrawlerHints,
|
crawler_hints: CrawlerHints,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HtmlResponse {
|
impl Html {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
status: StatusCode::OK,
|
status: StatusCode::OK,
|
||||||
@@ -117,13 +118,13 @@ impl HtmlResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HtmlResponse {
|
impl Default for Html {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for HtmlResponse {
|
impl IntoResponse for Html {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let html_doc = html! {
|
let html_doc = html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
@@ -145,7 +146,7 @@ impl IntoResponse for HtmlResponse {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(self.status, Html(html_doc.into_string()))
|
(self.status, axum::response::Html(html_doc.into_string()))
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,3 +262,29 @@ impl Render for CrawlerHints {
|
|||||||
let _result = self.write_meta_list_to(escaper);
|
let _result = self.write_meta_list_to(escaper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Rss<T>(pub T);
|
||||||
|
|
||||||
|
impl<T: Into<Full<Bytes>>> IntoResponse for Rss<T> {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let headers = [
|
||||||
|
(header::CONTENT_TYPE, HeaderValue::from_static("application/rss+xml")),
|
||||||
|
];
|
||||||
|
|
||||||
|
(headers, self.0.into())
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Atom<T>(pub T);
|
||||||
|
|
||||||
|
impl<T: Into<Full<Bytes>>> IntoResponse for Atom<T> {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let headers = [
|
||||||
|
(header::CONTENT_TYPE, HeaderValue::from_static("application/atom+xml")),
|
||||||
|
];
|
||||||
|
|
||||||
|
(headers, self.0.into())
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::Bytes,
|
||||||
|
extract::Extension,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::response::Rss;
|
||||||
|
use crate::{posts_store::ConcurrentPostsStore, Config};
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
Extension(config): Extension<Arc<Config>>,
|
||||||
|
Extension(posts): Extension<ConcurrentPostsStore>,
|
||||||
|
) -> Rss<Bytes> {
|
||||||
|
let rss_items = posts.read()
|
||||||
|
.await
|
||||||
|
.iter_by_created()
|
||||||
|
.take(config.rss.num_posts)
|
||||||
|
.map(|post| {
|
||||||
|
rss::ItemBuilder::default()
|
||||||
|
.title(Some(post.title().to_owned()))
|
||||||
|
.link(Some(format!(
|
||||||
|
"{}://{}/articles/{}",
|
||||||
|
config.rss.protocol,
|
||||||
|
config.rss.domain,
|
||||||
|
post.id()
|
||||||
|
)))
|
||||||
|
.pub_date(Some(post.created().to_rfc2822()))
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
.collect::<Vec<rss::Item>>();
|
||||||
|
|
||||||
|
Rss(rss::ChannelBuilder::default()
|
||||||
|
.title(config.rss.title.clone())
|
||||||
|
.link(format!(
|
||||||
|
"{}://{}",
|
||||||
|
config.rss.protocol, config.rss.domain
|
||||||
|
))
|
||||||
|
.ttl(Some(config.rss.ttl.to_string()))
|
||||||
|
.items(rss_items)
|
||||||
|
.build()
|
||||||
|
.to_string()
|
||||||
|
.into())
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::path::Path;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
handler::Handler,
|
handler::Handler,
|
||||||
@@ -11,35 +11,40 @@ use tower::limit::ConcurrencyLimitLayer;
|
|||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::posts_store::ConcurrentPostsStore;
|
use crate::{
|
||||||
|
Config,
|
||||||
|
posts_store::ConcurrentPostsStore
|
||||||
|
};
|
||||||
use super::{
|
use super::{
|
||||||
contact,
|
contact,
|
||||||
index,
|
index,
|
||||||
post,
|
post,
|
||||||
posts_list,
|
posts_list,
|
||||||
response::ErrorResponse,
|
response::Error,
|
||||||
|
rss,
|
||||||
static_content,
|
static_content,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn service(
|
pub fn service(
|
||||||
|
config: Config,
|
||||||
posts_store: ConcurrentPostsStore,
|
posts_store: ConcurrentPostsStore,
|
||||||
static_dir: &Path,
|
|
||||||
concurrency_limit: usize
|
|
||||||
) -> Router
|
) -> Router
|
||||||
{
|
{
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(index::handle))
|
.route("/", get(index::handle))
|
||||||
|
.route("/rss.xml", get(rss::handle))
|
||||||
.route("/contact", get(contact::handle))
|
.route("/contact", get(contact::handle))
|
||||||
.route("/articles", get(posts_list::handle))
|
.route("/articles", get(posts_list::handle))
|
||||||
.route("/articles/:post_id", get(post::handle))
|
.route("/articles/:post_id", get(post::handle))
|
||||||
.nest("/static", static_content::service(static_dir))
|
.nest("/static", static_content::service(&config.static_dir))
|
||||||
.fallback(handle_fallback.into_service())
|
.fallback(handle_fallback.into_service())
|
||||||
.layer(ConcurrencyLimitLayer::new(concurrency_limit))
|
.layer(ConcurrencyLimitLayer::new(config.concurrency_limit))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(Extension(Arc::new(config)))
|
||||||
.layer(Extension(posts_store))
|
.layer(Extension(posts_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_fallback(uri: Uri) -> ErrorResponse {
|
pub async fn handle_fallback(uri: Uri) -> Error {
|
||||||
info!(path = %uri.path(), "Requested resource not found");
|
info!(path = %uri.path(), "Requested resource not found");
|
||||||
ErrorResponse::RouteNotFound
|
Error::RouteNotFound
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ use tower::ServiceExt;
|
|||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tracing::{info, error};
|
use tracing::{info, error};
|
||||||
|
|
||||||
use super::response::ErrorResponse;
|
use super::response::Error;
|
||||||
|
|
||||||
pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> {
|
pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> {
|
||||||
let fallback_service = handle_fallback
|
let fallback_service = handle_fallback
|
||||||
@@ -29,12 +29,12 @@ pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> {
|
|||||||
.handle_error(handle_error)
|
.handle_error(handle_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_fallback(uri: Uri) -> ErrorResponse {
|
pub async fn handle_fallback(uri: Uri) -> Error {
|
||||||
info!(path = %uri.path(), "Requested static file not found");
|
info!(path = %uri.path(), "Requested static file not found");
|
||||||
ErrorResponse::StaticResourceNotFound
|
Error::StaticResourceNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_error(uri: Uri, err: io::Error) -> ErrorResponse {
|
pub async fn handle_error(uri: Uri, err: io::Error) -> Error {
|
||||||
error!(path = %uri.path(), err = %err, "IO error");
|
error!(path = %uri.path(), err = %err, "IO error");
|
||||||
ErrorResponse::Internal
|
Error::Internal
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user