Update
This commit is contained in:
parent
584c07ff23
commit
53e144d9a7
24 changed files with 1236 additions and 254 deletions
|
@ -1,4 +1,3 @@
|
|||
use diesel::QueryDsl;
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
use tikv_jemallocator::Jemalloc;
|
||||
|
||||
|
@ -6,104 +5,23 @@ use tikv_jemallocator::Jemalloc;
|
|||
#[global_allocator]
|
||||
static GLOBAL: Jemalloc = Jemalloc;
|
||||
|
||||
use actix_web::{get, http::StatusCode, middleware, web, App, HttpResponse, HttpServer};
|
||||
use actix_web_static_files::ResourceFiles;
|
||||
use askama_actix::{Template, TemplateToResponse};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use backend::*;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
|
||||
mod filters {
|
||||
pub fn cmark<T: std::fmt::Display>(s: T) -> ::askama::Result<String> {
|
||||
let s = s.to_string();
|
||||
|
||||
let options = pulldown_cmark::Options::empty();
|
||||
let parser = pulldown_cmark::Parser::new_ext(&s, options);
|
||||
|
||||
let mut html_output = String::with_capacity(s.len() * 3 / 2);
|
||||
pulldown_cmark::html::push_html(&mut html_output, parser);
|
||||
|
||||
Ok(html_output)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "status_code.html")]
|
||||
struct StatusCodeTemplate {
|
||||
status_code: StatusCode,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "posts/{slug}.html")]
|
||||
struct PostBySlugTemplate {
|
||||
post: db::models::Post,
|
||||
}
|
||||
|
||||
async fn not_found() -> HttpResponse {
|
||||
StatusCodeTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
message: None,
|
||||
}
|
||||
.to_response()
|
||||
}
|
||||
|
||||
#[get("/posts/{slug}")]
|
||||
async fn post_by_slug(db_pool: web::Data<db::DbPool>, path: web::Path<String>) -> HttpResponse {
|
||||
let slug = path.into_inner();
|
||||
|
||||
let conn = &mut match db_pool.get() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||
};
|
||||
|
||||
let post = match db::schema::posts::table
|
||||
.filter(db::schema::posts::slug.eq(&slug))
|
||||
.filter(db::schema::posts::active)
|
||||
.first::<db::models::Post>(conn)
|
||||
.optional()
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
dbg!(&e);
|
||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
match post {
|
||||
Some(x) => PostBySlugTemplate { post: x }.to_response(),
|
||||
None => {
|
||||
let mut resp = StatusCodeTemplate {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
message: None,
|
||||
}
|
||||
.to_response();
|
||||
*resp.status_mut() = StatusCode::NOT_FOUND;
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
}
|
||||
use actix_web::{middleware, App, HttpServer};
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::init();
|
||||
log::info!("Listening to requests at http://0.0.0.0:80");
|
||||
log::info!(
|
||||
"Listening to requests at {}",
|
||||
backend::config::CONFIG.bind_url
|
||||
);
|
||||
|
||||
HttpServer::new(move || {
|
||||
let generated = generate();
|
||||
|
||||
App::new()
|
||||
.wrap(middleware::Compress::default())
|
||||
.wrap(middleware::Logger::default())
|
||||
.app_data(web::Data::new(db::pool().unwrap()))
|
||||
.service(post_by_slug)
|
||||
.service(ResourceFiles::new("/static", generated))
|
||||
.default_service(web::route().to(not_found))
|
||||
.configure(backend::web::init)
|
||||
})
|
||||
.bind("0.0.0.0:80")
|
||||
.bind(&backend::config::CONFIG.bind_url)
|
||||
.unwrap()
|
||||
.run()
|
||||
.await
|
||||
|
|
|
@ -9,6 +9,7 @@ use anyhow::{bail, Result};
|
|||
use chrono::prelude::*;
|
||||
use clap::{Parser, Subcommand};
|
||||
use diesel::prelude::*;
|
||||
use r2d2_redis::redis;
|
||||
use scan_dir::ScanDir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
|
@ -27,6 +28,9 @@ struct Cli {
|
|||
enum Commands {
|
||||
#[clap(about = "Imports new posts")]
|
||||
Import,
|
||||
|
||||
#[clap(about = "Clears redis cache")]
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
|
@ -50,88 +54,137 @@ struct Post {
|
|||
fn main() -> Result<()> {
|
||||
match Cli::parse().commands {
|
||||
Commands::Import => {
|
||||
let conn = &mut db::establish_connection()?;
|
||||
let db_conn = &mut db::establish_connection()?;
|
||||
let redis_conn = &mut cache::establish_connection()?;
|
||||
|
||||
for post in ScanDir::dirs().read("/blog", |iter| {
|
||||
iter.map(|(entry, _)| {
|
||||
let path = entry.path().join("post.md");
|
||||
let src = fs::read_to_string(&path)?;
|
||||
let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) {
|
||||
Ok(x) => x,
|
||||
Err(x) => bail!("Error parsing frontmatter: {:?}", x),
|
||||
};
|
||||
let posts = ScanDir::dirs()
|
||||
.read("/blog", |iter| {
|
||||
iter.map(|(entry, _)| {
|
||||
let path = entry.path().join("post.md");
|
||||
let src = fs::read_to_string(&path)?;
|
||||
let frontmatter = match fronma::parser::parse::<PostFrontmatter>(&src) {
|
||||
Ok(x) => x,
|
||||
Err(x) => bail!("Error parsing frontmatter: {:?}", x),
|
||||
};
|
||||
|
||||
Ok(Post {
|
||||
path,
|
||||
frontmatter: frontmatter.headers,
|
||||
content: frontmatter.body.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})?? {
|
||||
let content = post.content.trim();
|
||||
|
||||
if let Some(id) = post.frontmatter.id {
|
||||
diesel::update(db::schema::posts::table)
|
||||
.filter(db::schema::posts::id.eq(id))
|
||||
.set(db::models::UpdatePost {
|
||||
name: Some(&post.frontmatter.name),
|
||||
slug: Some(&post.frontmatter.slug),
|
||||
description: Some(&post.frontmatter.description),
|
||||
content: Some(content),
|
||||
published_at: Some(post.frontmatter.published_at),
|
||||
edited_at: Some(post.frontmatter.edited_at),
|
||||
active: Some(post.frontmatter.active),
|
||||
Ok(Post {
|
||||
path,
|
||||
frontmatter: frontmatter.headers,
|
||||
content: frontmatter.body.to_string(),
|
||||
})
|
||||
.execute(conn)?;
|
||||
} else {
|
||||
let id = if let Some(id) = db::schema::posts::table
|
||||
.select(db::schema::posts::id)
|
||||
.filter(db::schema::posts::slug.eq(&post.frontmatter.slug))
|
||||
.first::<i32>(conn)
|
||||
.optional()?
|
||||
{
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})??
|
||||
.into_iter()
|
||||
.map(|post| -> Result<_> {
|
||||
let trimmed = PostFrontmatter {
|
||||
id: post.frontmatter.id,
|
||||
name: post.frontmatter.name.trim().to_string(),
|
||||
slug: post.frontmatter.slug.trim().to_string(),
|
||||
description: post.frontmatter.description.trim().to_string(),
|
||||
published_at: post.frontmatter.published_at,
|
||||
edited_at: post.frontmatter.edited_at,
|
||||
active: post.frontmatter.active,
|
||||
};
|
||||
let content = post.content.trim();
|
||||
|
||||
if let Some(id) = trimmed.id {
|
||||
diesel::update(db::schema::posts::table)
|
||||
.filter(db::schema::posts::id.eq(id))
|
||||
.set(db::models::UpdatePost {
|
||||
name: Some(&post.frontmatter.name),
|
||||
slug: None,
|
||||
description: Some(&post.frontmatter.description),
|
||||
name: Some(&trimmed.name),
|
||||
slug: Some(&trimmed.slug),
|
||||
description: Some(&trimmed.description),
|
||||
content: Some(content),
|
||||
published_at: Some(post.frontmatter.published_at),
|
||||
edited_at: Some(post.frontmatter.edited_at),
|
||||
active: Some(post.frontmatter.active),
|
||||
published_at: Some(trimmed.published_at),
|
||||
edited_at: Some(trimmed.edited_at),
|
||||
active: Some(trimmed.active),
|
||||
})
|
||||
.execute(conn)?;
|
||||
.execute(db_conn)?;
|
||||
|
||||
id
|
||||
Ok(id)
|
||||
} else {
|
||||
diesel::insert_into(db::schema::posts::table)
|
||||
.values(db::models::NewPost {
|
||||
name: &post.frontmatter.name,
|
||||
slug: &post.frontmatter.slug,
|
||||
description: &post.frontmatter.description,
|
||||
content: content,
|
||||
published_at: post.frontmatter.published_at,
|
||||
edited_at: post.frontmatter.edited_at,
|
||||
active: post.frontmatter.active,
|
||||
})
|
||||
.returning(db::schema::posts::id)
|
||||
.get_result::<i32>(conn)?
|
||||
};
|
||||
let id = if let Some(id) = db::schema::posts::table
|
||||
.select(db::schema::posts::id)
|
||||
.filter(db::schema::posts::slug.eq(&trimmed.slug))
|
||||
.first::<i32>(db_conn)
|
||||
.optional()?
|
||||
{
|
||||
diesel::update(db::schema::posts::table)
|
||||
.filter(db::schema::posts::id.eq(id))
|
||||
.set(db::models::UpdatePost {
|
||||
name: Some(&trimmed.name),
|
||||
slug: None,
|
||||
description: Some(&trimmed.description),
|
||||
content: Some(content),
|
||||
published_at: Some(trimmed.published_at),
|
||||
edited_at: Some(trimmed.edited_at),
|
||||
active: Some(trimmed.active),
|
||||
})
|
||||
.execute(db_conn)?;
|
||||
|
||||
fs::write(
|
||||
post.path,
|
||||
format!(
|
||||
"---\n{}---\n{}",
|
||||
serde_yaml::to_string(&PostFrontmatter {
|
||||
id: Some(id),
|
||||
..post.frontmatter
|
||||
})?,
|
||||
post.content
|
||||
),
|
||||
)?;
|
||||
}
|
||||
id
|
||||
} else {
|
||||
diesel::insert_into(db::schema::posts::table)
|
||||
.values(db::models::NewPost {
|
||||
name: &trimmed.name,
|
||||
slug: &trimmed.slug,
|
||||
description: &trimmed.description,
|
||||
content: content,
|
||||
published_at: trimmed.published_at,
|
||||
edited_at: trimmed.edited_at,
|
||||
active: trimmed.active,
|
||||
})
|
||||
.returning(db::schema::posts::id)
|
||||
.get_result::<i32>(db_conn)?
|
||||
};
|
||||
|
||||
fs::write(
|
||||
post.path,
|
||||
format!(
|
||||
"---\n{}---\n\n{}\n",
|
||||
serde_yaml::to_string(&PostFrontmatter {
|
||||
id: Some(id),
|
||||
..trimmed
|
||||
})?,
|
||||
content
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let ids = db::schema::posts::table
|
||||
.select(db::schema::posts::id)
|
||||
.load::<i32>(db_conn)?;
|
||||
|
||||
diesel::delete(
|
||||
db::schema::posts::table
|
||||
.filter(diesel::dsl::not(db::schema::posts::id.eq_any(posts))),
|
||||
)
|
||||
.execute(db_conn)?;
|
||||
|
||||
for id in ids {
|
||||
redis::cmd("DEL")
|
||||
.arg(cache::keys::post_content(id))
|
||||
.query::<()>(redis_conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Commands::Clear => {
|
||||
let db_conn = &mut db::establish_connection()?;
|
||||
let redis_conn = &mut cache::establish_connection()?;
|
||||
|
||||
for id in db::schema::posts::table
|
||||
.select(db::schema::posts::id)
|
||||
.load::<i32>(db_conn)?
|
||||
{
|
||||
redis::cmd("DEL")
|
||||
.arg(cache::keys::post_content(id))
|
||||
.query::<()>(redis_conn)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
28
backend/src/cache.rs
Normal file
28
backend/src/cache.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use anyhow::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use r2d2_redis::{r2d2, redis, RedisConnectionManager};
|
||||
|
||||
use crate::config;
|
||||
|
||||
pub type RedisPool = r2d2::Pool<RedisConnectionManager>;
|
||||
pub type ConnectionPool = r2d2::PooledConnection<RedisConnectionManager>;
|
||||
|
||||
pub fn establish_connection() -> Result<redis::Connection> {
|
||||
Ok(redis::Client::open(config::CONFIG.redis_url.as_str())?.get_connection()?)
|
||||
}
|
||||
|
||||
pub fn pool() -> Result<RedisPool> {
|
||||
Ok(r2d2::Pool::builder().build(RedisConnectionManager::new(
|
||||
config::CONFIG.redis_url.as_str(),
|
||||
)?)?)
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref POOL: RedisPool = pool().unwrap();
|
||||
}
|
||||
|
||||
pub mod keys {
|
||||
pub fn post_content(id: i32) -> String {
|
||||
format!("post_content:{}", id)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,19 @@
|
|||
|
||||
use envconfig::Envconfig;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
#[derive(Envconfig, Debug)]
|
||||
pub struct Config {
|
||||
#[envconfig(from = "BACKEND_BIND_URL")]
|
||||
pub bind_url: String,
|
||||
|
||||
#[envconfig(from = "BACKEND_DB_URL")]
|
||||
pub db_url: String,
|
||||
|
||||
#[envconfig(from = "BACKEND_REDIS_URL")]
|
||||
pub redis_url: String,
|
||||
|
||||
#[envconfig(from = "BACKEND_CACHE_POST_CONTENT_TTL")]
|
||||
pub cache_post_content_ttl: usize,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
pub mod cache;
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod markdown;
|
||||
pub mod web;
|
||||
|
|
28
backend/src/markdown.rs
Normal file
28
backend/src/markdown.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
pub fn to_html(src: &str) -> String {
|
||||
let adapter = comrak::plugins::syntect::SyntectAdapter::new("InspiredGitHub");
|
||||
let options = comrak::ComrakOptions {
|
||||
extension: comrak::ComrakExtensionOptions {
|
||||
strikethrough: true,
|
||||
tagfilter: true,
|
||||
table: true,
|
||||
autolink: true,
|
||||
tasklist: true,
|
||||
superscript: true,
|
||||
header_ids: Some("__blog-content_".to_string()),
|
||||
footnotes: true,
|
||||
description_lists: true,
|
||||
front_matter_delimiter: None,
|
||||
shortcodes: true,
|
||||
},
|
||||
parse: comrak::ComrakParseOptions {
|
||||
smart: true,
|
||||
..comrak::ComrakParseOptions::default()
|
||||
},
|
||||
..comrak::ComrakOptions::default()
|
||||
};
|
||||
let mut plugins = comrak::ComrakPlugins::default();
|
||||
|
||||
plugins.render.codefence_syntax_highlighter = Some(&adapter);
|
||||
|
||||
comrak::markdown_to_html_with_plugins(src, &options, &plugins)
|
||||
}
|
176
backend/src/web/mod.rs
Normal file
176
backend/src/web/mod.rs
Normal file
|
@ -0,0 +1,176 @@
|
|||
use actix_web::{get, http, web, HttpResponse};
|
||||
use actix_web_static_files::ResourceFiles;
|
||||
use askama_actix::TemplateToResponse;
|
||||
use diesel::prelude::*;
|
||||
use r2d2_redis::redis;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use crate::{cache, config, db, markdown};
|
||||
|
||||
pub mod templates;
|
||||
|
||||
pub mod static_dir {
|
||||
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
|
||||
}
|
||||
|
||||
async fn not_found() -> HttpResponse {
|
||||
let mut resp = templates::StatusCode {
|
||||
status_code: http::StatusCode::NOT_FOUND,
|
||||
message: Some("maybe try a correct url?".to_string()),
|
||||
}
|
||||
.to_response();
|
||||
*resp.status_mut() = http::StatusCode::NOT_FOUND;
|
||||
|
||||
resp
|
||||
}
|
||||
|
||||
#[get("/posts")]
|
||||
async fn posts(db_pool: web::Data<db::DbPool>) -> HttpResponse {
|
||||
let db_conn = &mut match db_pool.get() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||
};
|
||||
|
||||
let posts = match db::schema::posts::table
|
||||
.filter(db::schema::posts::active)
|
||||
.order(db::schema::posts::published_at.desc())
|
||||
.load::<db::models::Post>(db_conn)
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
templates::Posts { posts }.to_response()
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index() -> HttpResponse {
|
||||
templates::Index.to_response()
|
||||
}
|
||||
|
||||
#[get("/about")]
|
||||
async fn about() -> HttpResponse {
|
||||
templates::About.to_response()
|
||||
}
|
||||
|
||||
#[get("/posts/{slug}")]
|
||||
async fn post_by_slug(
|
||||
db_pool: web::Data<db::DbPool>,
|
||||
redis_pool: web::Data<cache::RedisPool>,
|
||||
path: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
let slug = path.into_inner();
|
||||
|
||||
let db_conn = &mut match db_pool.get() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||
};
|
||||
let redis_conn = &mut match redis_pool.get() {
|
||||
Ok(x) => x,
|
||||
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||
};
|
||||
|
||||
let post_stripped: Option<(i32, String)> = match db::schema::posts::table
|
||||
.select((db::schema::posts::id, db::schema::posts::name))
|
||||
.filter(db::schema::posts::slug.eq(&slug))
|
||||
.filter(db::schema::posts::active)
|
||||
.get_result::<(i32, String)>(db_conn)
|
||||
.optional()
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
match post_stripped {
|
||||
Some(stripped) => {
|
||||
let (stripped_id, stripped_name) = stripped;
|
||||
|
||||
let key = cache::keys::post_content(stripped_id);
|
||||
|
||||
match match redis::cmd("GET")
|
||||
.arg(&key)
|
||||
.query::<Option<String>>(redis_conn.deref_mut())
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(e) => return HttpResponse::InternalServerError().body(format!("{:?}", e)),
|
||||
} {
|
||||
Some(s) => templates::PostBySlug {
|
||||
name: stripped_name,
|
||||
slug,
|
||||
content: s,
|
||||
}
|
||||
.to_response(),
|
||||
None => {
|
||||
let post = match db::schema::posts::table
|
||||
.filter(db::schema::posts::id.eq(stripped_id))
|
||||
.first::<db::models::Post>(db_conn)
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let html = markdown::to_html(&post.content);
|
||||
|
||||
match redis::cmd("SET")
|
||||
.arg(&key)
|
||||
.arg(&html)
|
||||
.query::<Option<String>>(redis_conn.deref_mut())
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
return HttpResponse::InternalServerError().body(format!("{:?}", e))
|
||||
}
|
||||
};
|
||||
if let Err(e) = redis::cmd("EXPIRE")
|
||||
.arg(key)
|
||||
.arg(config::CONFIG.cache_post_content_ttl)
|
||||
.query::<()>(redis_conn.deref_mut())
|
||||
{
|
||||
return HttpResponse::InternalServerError().body(format!("{:?}", e));
|
||||
}
|
||||
|
||||
templates::PostBySlug {
|
||||
name: post.name,
|
||||
slug: post.slug,
|
||||
content: html,
|
||||
}
|
||||
.to_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let mut resp = templates::StatusCode {
|
||||
status_code: http::StatusCode::NOT_FOUND,
|
||||
message: Some("this post does not exists... yet".to_string()),
|
||||
}
|
||||
.to_response();
|
||||
*resp.status_mut() = http::StatusCode::NOT_FOUND;
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_routes(cfg: &mut web::ServiceConfig) {
|
||||
let generated = static_dir::generate();
|
||||
|
||||
cfg.service(index)
|
||||
.service(about)
|
||||
.service(posts)
|
||||
.service(ResourceFiles::new("/static", generated))
|
||||
.service(post_by_slug)
|
||||
.default_service(web::route().to(not_found));
|
||||
}
|
||||
|
||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||
cfg.app_data(web::Data::new(db::pool().unwrap()))
|
||||
.app_data(web::Data::new(cache::pool().unwrap()));
|
||||
|
||||
setup_routes(cfg);
|
||||
}
|
33
backend/src/web/templates.rs
Normal file
33
backend/src/web/templates.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use actix_web::http;
|
||||
use askama_actix::Template;
|
||||
|
||||
use crate::db;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "status_code.html")]
|
||||
pub struct StatusCode {
|
||||
pub status_code: http::StatusCode,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "web/index.html")]
|
||||
pub struct Index;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "web/about.html")]
|
||||
pub struct About;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "web/posts/index.html")]
|
||||
pub struct Posts {
|
||||
pub posts: Vec<db::models::Post>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "web/posts/{slug}.html")]
|
||||
pub struct PostBySlug {
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub content: String,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue