Styling for index page

This commit is contained in:
Pantonshire committed 2022-05-18 15:30:26 +01:00
1 parent d566d02a4f
commit 79a248d641
15 files changed
+355 -27

No files matched your search

+52
View File
@@ -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()) }
}
}
}
}
}))
}
+7
View File
@@ -0,0 +1,7 @@
mod index;
mod posts;
mod response;
mod site;
mod static_content;
pub use site::service as site_service;
+29
View File
@@ -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())
}
}))
}
+263
View File
@@ -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);
}
}
+41
View File
@@ -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
}
+40
View File
@@ -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
}