Styling for index page
This commit is contained in:
1 parent
d566d02a4f
commit
79a248d641
15 files changed
+355
-27
No files matched your search
@@ -0,0 +1,52 @@
|
||||
use axum::extract::Extension;
|
||||
use maud::html;
|
||||
|
||||
use crate::{
|
||||
posts_store::ConcurrentPostsStore,
|
||||
template,
|
||||
};
|
||||
use super::response::HtmlResponse;
|
||||
|
||||
pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> HtmlResponse {
|
||||
HtmlResponse::new()
|
||||
.with_title_static("Placeholder title")
|
||||
.with_crawler_permissive()
|
||||
.with_head(html! {
|
||||
link href="/static/styles/main.css" rel="stylesheet";
|
||||
})
|
||||
.with_body(template::main_page(html! {
|
||||
section .content_section {
|
||||
h2 { "Who I am" }
|
||||
p {
|
||||
"Hi! I'm Tom, a computer science student and hobbyist programmer."
|
||||
}
|
||||
figure {
|
||||
img src="/static/images/tombot_450.png"
|
||||
alt="A drawing by @smolrobots (on Twitter) of me as a robot. It has an \
|
||||
egg-shaped body and a smiling face, and is doing a little dance next \
|
||||
to a Raspberry Pi."
|
||||
width="256";
|
||||
figcaption {
|
||||
"Drawn by "
|
||||
a href="https://twitter.com/smolrobots" { "@smolrobots" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section .content_section {
|
||||
h2 { "Things I've made" }
|
||||
p { "Todo" }
|
||||
}
|
||||
|
||||
section .content_section {
|
||||
h2 { "Articles" }
|
||||
ul {
|
||||
@for post in posts.read().await.iter_by_created().rev() {
|
||||
li {
|
||||
a href={"/posts/" (post.id_str())} { (post.title()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod index;
|
||||
mod posts;
|
||||
mod response;
|
||||
mod site;
|
||||
mod static_content;
|
||||
|
||||
pub use site::service as site_service;
|
||||
@@ -0,0 +1,29 @@
|
||||
use axum::extract::{Extension, Path};
|
||||
use maud::html;
|
||||
|
||||
use crate::posts_store::ConcurrentPostsStore;
|
||||
use super::response::{ErrorResponse, HtmlResponse};
|
||||
|
||||
pub async fn handle(
|
||||
Path(post_id): Path<String>,
|
||||
Extension(posts): Extension<ConcurrentPostsStore>
|
||||
) -> Result<HtmlResponse, ErrorResponse>
|
||||
{
|
||||
let post = posts.get(&post_id)
|
||||
.await
|
||||
.ok_or(ErrorResponse::PostNotFound)?;
|
||||
|
||||
Ok(HtmlResponse::new()
|
||||
.with_crawler_permissive()
|
||||
.with_title_owned(post.title().to_owned())
|
||||
.with_head(html! {
|
||||
link href="/static/style/code.css" rel="stylesheet";
|
||||
})
|
||||
.with_body(html! {
|
||||
h1 { (post.title()) }
|
||||
p { "by " (post.author()) }
|
||||
article {
|
||||
(post.html())
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Write},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Html, Response},
|
||||
};
|
||||
use maud::{html, Markup, Render, Escaper, DOCTYPE};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorResponse {
|
||||
Internal,
|
||||
PostNotFound,
|
||||
StaticResourceNotFound,
|
||||
RouteNotFound,
|
||||
}
|
||||
|
||||
impl ErrorResponse {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
ErrorResponse::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ErrorResponse::PostNotFound => StatusCode::NOT_FOUND,
|
||||
ErrorResponse::StaticResourceNotFound => StatusCode::NOT_FOUND,
|
||||
ErrorResponse::RouteNotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ErrorResponse {
|
||||
fn into_response(self) -> Response {
|
||||
let status_code = self.status_code();
|
||||
|
||||
// Create a string buffer containing the full error text, e.g. "404 Not Found".
|
||||
let status_text = {
|
||||
let status_code_str = status_code.as_str();
|
||||
let reason = status_code.canonical_reason();
|
||||
|
||||
// Allocate a buffer with enough capacity to store the full error text.
|
||||
let mut buf = String::with_capacity(
|
||||
status_code_str.len() + reason.map(|reason| reason.len() + 1).unwrap_or(0));
|
||||
|
||||
// Push the numerical code string first, then a space, then the error reason string.
|
||||
buf.push_str(status_code_str);
|
||||
if let Some(reason) = reason {
|
||||
buf.push(' ');
|
||||
buf.push_str(reason);
|
||||
}
|
||||
|
||||
buf
|
||||
};
|
||||
|
||||
HtmlResponse::new()
|
||||
.with_status(status_code)
|
||||
.with_body(html! {
|
||||
p { (status_text) }
|
||||
})
|
||||
.with_title_owned(status_text)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HtmlResponse {
|
||||
status: StatusCode,
|
||||
title: Cow<'static, str>,
|
||||
head: Option<Markup>,
|
||||
body: Option<Markup>,
|
||||
crawler_hints: CrawlerHints,
|
||||
}
|
||||
|
||||
impl HtmlResponse {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
status: StatusCode::OK,
|
||||
title: Cow::Borrowed("untitled"),
|
||||
head: None,
|
||||
body: None,
|
||||
crawler_hints: CrawlerHints::restrictive(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_status(self, status: StatusCode) -> Self {
|
||||
Self { status, ..self }
|
||||
}
|
||||
|
||||
pub fn with_title(self, title: Cow<'static, str>) -> Self {
|
||||
Self { title, ..self }
|
||||
}
|
||||
|
||||
pub fn with_title_static(self, title: &'static str) -> Self {
|
||||
self.with_title(Cow::Borrowed(title))
|
||||
}
|
||||
|
||||
pub fn with_title_owned(self, title: String) -> Self {
|
||||
self.with_title(Cow::Owned(title))
|
||||
}
|
||||
|
||||
pub fn with_head(self, head: Markup) -> Self {
|
||||
Self { head: Some(head), ..self }
|
||||
}
|
||||
|
||||
pub fn with_body(self, body: Markup) -> Self {
|
||||
Self { body: Some(body), ..self }
|
||||
}
|
||||
|
||||
pub fn with_crawler_hints(self, crawler_hints: CrawlerHints) -> Self {
|
||||
Self { crawler_hints, ..self }
|
||||
}
|
||||
|
||||
pub fn with_crawler_restrictive(self) -> Self {
|
||||
self.with_crawler_hints(CrawlerHints::restrictive())
|
||||
}
|
||||
|
||||
pub fn with_crawler_permissive(self) -> Self {
|
||||
self.with_crawler_hints(CrawlerHints::permissive())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HtmlResponse {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for HtmlResponse {
|
||||
fn into_response(self) -> Response {
|
||||
let html_doc = html! {
|
||||
(DOCTYPE)
|
||||
html {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
meta name="robots" content=(self.crawler_hints);
|
||||
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||
title { (self.title) }
|
||||
@if let Some(head) = self.head {
|
||||
(head)
|
||||
}
|
||||
}
|
||||
body {
|
||||
@if let Some(body) = self.body {
|
||||
(body)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(self.status, Html(html_doc.into_string()))
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CrawlerHints {
|
||||
index: bool,
|
||||
follow: bool,
|
||||
archive: bool,
|
||||
snippet: bool,
|
||||
image_index: bool,
|
||||
}
|
||||
|
||||
impl CrawlerHints {
|
||||
pub const fn restrictive() -> Self {
|
||||
Self {
|
||||
index: false,
|
||||
follow: false,
|
||||
archive: false,
|
||||
snippet: false,
|
||||
image_index: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn permissive() -> Self {
|
||||
Self {
|
||||
index: true,
|
||||
follow: true,
|
||||
archive: true,
|
||||
snippet: true,
|
||||
image_index: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_index(self, index: bool) -> Self {
|
||||
Self { index, ..self }
|
||||
}
|
||||
|
||||
pub const fn with_follow(self, follow: bool) -> Self {
|
||||
Self { follow, ..self }
|
||||
}
|
||||
|
||||
pub const fn with_archive(self, archive: bool) -> Self {
|
||||
Self { archive, ..self }
|
||||
}
|
||||
|
||||
pub const fn with_snippet(self, snippet: bool) -> Self {
|
||||
Self { snippet, ..self }
|
||||
}
|
||||
|
||||
pub const fn with_image_index(self, image_index: bool) -> Self {
|
||||
Self { image_index, ..self }
|
||||
}
|
||||
|
||||
fn index_str(self) -> &'static str {
|
||||
if self.index {
|
||||
"index"
|
||||
} else {
|
||||
"noindex"
|
||||
}
|
||||
}
|
||||
|
||||
fn follow_str(self) -> &'static str {
|
||||
if self.follow {
|
||||
"follow"
|
||||
} else {
|
||||
"nofollow"
|
||||
}
|
||||
}
|
||||
|
||||
fn archive_strs(self) -> Option<[&'static str; 2]> {
|
||||
if self.archive {
|
||||
None
|
||||
} else {
|
||||
Some(["noarchive", "nocache"])
|
||||
}
|
||||
}
|
||||
|
||||
fn snippet_str(self) -> Option<&'static str> {
|
||||
if self.snippet {
|
||||
None
|
||||
} else {
|
||||
Some("nosnippet")
|
||||
}
|
||||
}
|
||||
|
||||
fn image_index_str(self) -> Option<&'static str> {
|
||||
if self.image_index {
|
||||
None
|
||||
} else {
|
||||
Some("noimageindex")
|
||||
}
|
||||
}
|
||||
|
||||
fn write_meta_list_to<W: Write>(self, mut buf: W) -> fmt::Result {
|
||||
write!(buf, "{},{}", self.index_str(), self.follow_str())?;
|
||||
if let Some([archive_str, cache_str]) = self.archive_strs() {
|
||||
write!(buf, ",{},{}", archive_str, cache_str)?;
|
||||
}
|
||||
if let Some(snippet_str) = self.snippet_str() {
|
||||
write!(buf, ",{}", snippet_str)?;
|
||||
}
|
||||
if let Some(image_index_str) = self.image_index_str() {
|
||||
write!(buf, ",{}", image_index_str)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CrawlerHints {
|
||||
fn render_to(&self, buf: &mut String) {
|
||||
let escaper = Escaper::new(buf);
|
||||
let _result = self.write_meta_list_to(escaper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use std::path::Path;
|
||||
|
||||
use axum::{
|
||||
handler::Handler,
|
||||
http::Uri,
|
||||
extract::Extension,
|
||||
Router,
|
||||
routing::get,
|
||||
};
|
||||
use tower::limit::ConcurrencyLimitLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::info;
|
||||
|
||||
use crate::posts_store::ConcurrentPostsStore;
|
||||
use super::{
|
||||
index,
|
||||
posts,
|
||||
response::ErrorResponse,
|
||||
static_content,
|
||||
};
|
||||
|
||||
pub fn service(
|
||||
posts_store: ConcurrentPostsStore,
|
||||
static_dir: &Path,
|
||||
concurrency_limit: usize
|
||||
) -> Router
|
||||
{
|
||||
Router::new()
|
||||
.route("/", get(index::handle))
|
||||
.route("/posts/:post_id", get(posts::handle))
|
||||
.nest("/static", static_content::service(static_dir))
|
||||
.fallback(handle_fallback.into_service())
|
||||
.layer(ConcurrencyLimitLayer::new(concurrency_limit))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(Extension(posts_store))
|
||||
}
|
||||
|
||||
pub async fn handle_fallback(uri: Uri) -> ErrorResponse {
|
||||
info!(path = %uri.path(), "Requested resource not found");
|
||||
ErrorResponse::RouteNotFound
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
io,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
handler::Handler,
|
||||
http::Uri,
|
||||
routing::{get_service, MethodRouter},
|
||||
};
|
||||
use libshire::convert::Empty;
|
||||
use tower::ServiceExt;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing::{info, error};
|
||||
|
||||
use super::response::ErrorResponse;
|
||||
|
||||
pub fn service(static_dir: &Path) -> MethodRouter<Body, Infallible> {
|
||||
let fallback_service = handle_fallback
|
||||
.into_service()
|
||||
.map_err(Empty::elim::<io::Error>);
|
||||
|
||||
let serve_dir = ServeDir::new(static_dir)
|
||||
.fallback(fallback_service);
|
||||
|
||||
get_service(serve_dir)
|
||||
.handle_error(handle_error)
|
||||
}
|
||||
|
||||
pub async fn handle_fallback(uri: Uri) -> ErrorResponse {
|
||||
info!(path = %uri.path(), "Requested static file not found");
|
||||
ErrorResponse::StaticResourceNotFound
|
||||
}
|
||||
|
||||
pub async fn handle_error(uri: Uri, err: io::Error) -> ErrorResponse {
|
||||
error!(path = %uri.path(), err = %err, "IO error");
|
||||
ErrorResponse::Internal
|
||||
}
|
||||
Reference in new issue
Block a user