more stuff
This commit is contained in:
parent
bdc291b539
commit
8efba600f5
7 changed files with 159 additions and 73 deletions
|
@ -6,6 +6,8 @@ services:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
- otel-collector
|
- otel-collector
|
||||||
|
environment:
|
||||||
|
TLX_OTLP_ENABLED: true
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: otel/opentelemetry-collector:latest
|
image: otel/opentelemetry-collector:latest
|
||||||
restart: always
|
restart: always
|
||||||
|
|
|
@ -8,13 +8,10 @@ use axum::{
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, FixedOffset};
|
use chrono::{DateTime, FixedOffset};
|
||||||
use opentelemetry::{global, metrics::Counter, KeyValue};
|
use opentelemetry::{global, metrics::Counter, KeyValue};
|
||||||
use tokio::sync::OnceCell;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tracing::{
|
use tracing::{instrument, log::error};
|
||||||
instrument,
|
|
||||||
log::{debug, error, info},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{AppState, WebsiteError};
|
use crate::{AppState, WebsiteError};
|
||||||
|
|
||||||
|
@ -24,9 +21,9 @@ pub mod tags;
|
||||||
pub static HIT_COUNTER: OnceCell<Counter<u64>> = OnceCell::const_new();
|
pub static HIT_COUNTER: OnceCell<Counter<u64>> = OnceCell::const_new();
|
||||||
|
|
||||||
async fn record_hit(method: String, path: String) {
|
async fn record_hit(method: String, path: String) {
|
||||||
let counter = HIT_COUNTER.get_or_init(|| async {
|
let counter = HIT_COUNTER
|
||||||
global::meter("tlxite").u64_counter("page_hit_count").init()
|
.get_or_init(|| async { global::meter("tlxite").u64_counter("page_hit_count").init() })
|
||||||
}).await;
|
.await;
|
||||||
|
|
||||||
counter.add(1, &[KeyValue::new("path", format!("{method} {path}"))]);
|
counter.add(1, &[KeyValue::new("path", format!("{method} {path}"))]);
|
||||||
}
|
}
|
||||||
|
@ -36,6 +33,7 @@ pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.merge(pages::router())
|
.merge(pages::router())
|
||||||
.merge(tags::router())
|
.merge(tags::router())
|
||||||
|
// .merge(pages::pages_router(state.pages.values()))
|
||||||
.merge(pages::alias_router(state.pages.values()))
|
.merge(pages::alias_router(state.pages.values()))
|
||||||
.route("/healthcheck", get(healthcheck))
|
.route("/healthcheck", get(healthcheck))
|
||||||
.route_service("/posts/:slug/*path", ServeDir::new("./"))
|
.route_service("/posts/:slug/*path", ServeDir::new("./"))
|
||||||
|
@ -59,7 +57,13 @@ pub async fn index(
|
||||||
})?;
|
})?;
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[(header::LAST_MODIFIED, state.startup_time.to_rfc2822())],
|
[
|
||||||
|
(
|
||||||
|
header::LAST_MODIFIED,
|
||||||
|
state.startup_time.to_rfc2822().as_str(),
|
||||||
|
),
|
||||||
|
(header::CACHE_CONTROL, "no-cache"),
|
||||||
|
],
|
||||||
Html(res),
|
Html(res),
|
||||||
)
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
|
@ -89,29 +93,23 @@ pub async fn metrics_middleware(request: Request, next: Next) -> Response {
|
||||||
|
|
||||||
fn should_return_304(headers: &HeaderMap, last_changed: Option<DateTime<FixedOffset>>) -> bool {
|
fn should_return_304(headers: &HeaderMap, last_changed: Option<DateTime<FixedOffset>>) -> bool {
|
||||||
let Some(date) = last_changed else {
|
let Some(date) = last_changed else {
|
||||||
debug!("no last modified date");
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(since) = headers.get(header::IF_MODIFIED_SINCE) else {
|
let Some(since) = headers.get(header::IF_MODIFIED_SINCE) else {
|
||||||
debug!("no IF_MODIFIED_SINCE header");
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(parsed) = DateTime::<FixedOffset>::parse_from_rfc2822(since.to_str().unwrap()) else {
|
let Ok(parsed) = DateTime::<FixedOffset>::parse_from_rfc2822(since.to_str().unwrap()) else {
|
||||||
debug!("failed to parse IF_MODIFIED_SINCE header");
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
date > parsed
|
date >= parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for WebsiteError {
|
impl IntoResponse for WebsiteError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
match self {
|
match self {
|
||||||
WebsiteError::NotFound => {
|
WebsiteError::NotFound => (StatusCode::NOT_FOUND, ()).into_response(),
|
||||||
info!("not found");
|
|
||||||
(StatusCode::NOT_FOUND, ()).into_response()
|
|
||||||
}
|
|
||||||
WebsiteError::InternalError(e) => {
|
WebsiteError::InternalError(e) => {
|
||||||
if let Some(s) = e.source() {
|
if let Some(s) = e.source() {
|
||||||
error!("internal error: {}: {}", e, s);
|
error!("internal error: {}: {}", e, s);
|
||||||
|
@ -148,7 +146,9 @@ mod tests {
|
||||||
};
|
};
|
||||||
// Load the actual posts, just to make this test fail if
|
// Load the actual posts, just to make this test fail if
|
||||||
// aliases overlap with themselves or other routes
|
// aliases overlap with themselves or other routes
|
||||||
let posts = crate::page::load_all(&state, "posts/".into()).await.unwrap();
|
let posts = crate::page::load_all(&state, "posts/".into())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
state.tags = crate::tag::get_tags(posts.values());
|
state.tags = crate::tag::get_tags(posts.values());
|
||||||
state.pages = posts;
|
state.pages = posts;
|
||||||
let state = Arc::new(state);
|
let state = Arc::new(state);
|
||||||
|
|
|
@ -2,17 +2,17 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::{header, HeaderMap, StatusCode},
|
http::{self, header, HeaderMap, StatusCode},
|
||||||
response::{Html, IntoResponse, Redirect, Response},
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde_derive::Serialize;
|
use serde_derive::Serialize;
|
||||||
use tracing::{instrument, log::warn};
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
page::{render_post, Page},
|
page::{render_page, Page},
|
||||||
AppState, WebsiteError,
|
AppState, WebsiteError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,10 +28,39 @@ pub fn router() -> Router<Arc<AppState>> {
|
||||||
.route("/posts/:slug/index.md", get(super::not_found))
|
.route("/posts/:slug/index.md", get(super::not_found))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alias_router<'a>(posts: impl IntoIterator<Item = &'a Page>) -> Router<Arc<AppState>> {
|
// pub fn pages_router<'a>(pages: impl IntoIterator<Item = &'a Page>) -> Router<Arc<AppState>> {
|
||||||
|
// let mut router = Router::new();
|
||||||
|
|
||||||
|
// for post in pages {
|
||||||
|
// let slug = post.slug.clone();
|
||||||
|
// router = router.route(
|
||||||
|
// &post.absolute_path,
|
||||||
|
// get(
|
||||||
|
// move |state: State<Arc<AppState>>,
|
||||||
|
// method: http::method::Method,
|
||||||
|
// headers: HeaderMap| async {
|
||||||
|
// view(Path(slug), state, method, headers).await
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// for alias in &post.aliases {
|
||||||
|
// let path = post.absolute_path.clone();
|
||||||
|
// router = router.route(
|
||||||
|
// alias,
|
||||||
|
// get(move || async {
|
||||||
|
// let p = path;
|
||||||
|
// Redirect::permanent(&p)
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// router
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn alias_router<'a>(pages: impl IntoIterator<Item = &'a Page>) -> Router<Arc<AppState>> {
|
||||||
let mut router = Router::new();
|
let mut router = Router::new();
|
||||||
|
|
||||||
for post in posts {
|
for post in pages {
|
||||||
for alias in &post.aliases {
|
for alias in &post.aliases {
|
||||||
let path = post.absolute_path.clone();
|
let path = post.absolute_path.clone();
|
||||||
router = router.route(
|
router = router.route(
|
||||||
|
@ -82,10 +111,15 @@ async fn index(
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[(
|
[
|
||||||
header::LAST_MODIFIED,
|
(
|
||||||
last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()),
|
header::LAST_MODIFIED,
|
||||||
)],
|
last_changed
|
||||||
|
.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822())
|
||||||
|
.as_str(),
|
||||||
|
),
|
||||||
|
(header::CACHE_CONTROL, "no-cache"),
|
||||||
|
],
|
||||||
Html(res),
|
Html(res),
|
||||||
)
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
|
@ -95,29 +129,42 @@ async fn index(
|
||||||
async fn view(
|
async fn view(
|
||||||
Path(slug): Path<String>,
|
Path(slug): Path<String>,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
|
method: http::method::Method,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<axum::response::Response, WebsiteError> {
|
) -> Result<axum::response::Response, WebsiteError> {
|
||||||
let post = state.pages.get(&slug).ok_or(WebsiteError::NotFound)?;
|
let post = state.pages.get(&slug).ok_or(WebsiteError::NotFound)?;
|
||||||
|
|
||||||
let last_changed = post.last_modified();
|
|
||||||
|
|
||||||
if should_return_304(&headers, last_changed) {
|
|
||||||
return Ok(StatusCode::NOT_MODIFIED.into_response());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !post.is_published() {
|
if !post.is_published() {
|
||||||
warn!("attempted to view post before it has been published!");
|
|
||||||
return Err(WebsiteError::NotFound);
|
return Err(WebsiteError::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = render_post(&state, post).await?;
|
if let Some(etag) = headers.get(header::IF_NONE_MATCH) {
|
||||||
|
if let Ok(etag) = etag.to_str() {
|
||||||
|
if etag == post.etag {
|
||||||
|
return Ok(StatusCode::NOT_MODIFIED.into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if method == http::method::Method::HEAD {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
[
|
||||||
|
(header::ETAG, post.etag.as_str()),
|
||||||
|
(header::CACHE_CONTROL, "no-cache"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = render_page(&state, post).await?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
[(
|
[
|
||||||
header::LAST_MODIFIED,
|
(header::ETAG, post.etag.as_str()),
|
||||||
last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()),
|
(header::CACHE_CONTROL, "no-cache"),
|
||||||
)],
|
],
|
||||||
Html(res),
|
Html(res),
|
||||||
)
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
|
|
44
src/main.rs
44
src/main.rs
|
@ -10,9 +10,9 @@ use settings::Settings;
|
||||||
use tag::Tag;
|
use tag::Tag;
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
|
|
||||||
use tokio::net::TcpListener;
|
use tokio::{net::TcpListener, signal};
|
||||||
use tower_http::{compression::CompressionLayer, cors::CorsLayer};
|
use tower_http::{compression::CompressionLayer, cors::CorsLayer};
|
||||||
use tracing::{instrument, log::info};
|
use tracing::{debug, instrument, log::info};
|
||||||
|
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
|
|
||||||
|
@ -44,7 +44,10 @@ async fn main() -> Result<()> {
|
||||||
info!("Starting server...");
|
info!("Starting server...");
|
||||||
let app = init_app(&cfg).await?;
|
let app = init_app(&cfg).await?;
|
||||||
let listener = TcpListener::bind(&cfg.bind_address).await.unwrap();
|
let listener = TcpListener::bind(&cfg.bind_address).await.unwrap();
|
||||||
axum::serve(listener, app.into_make_service()).await?;
|
|
||||||
|
axum::serve(listener, app.into_make_service())
|
||||||
|
.with_graceful_shutdown(shutdown_signal())
|
||||||
|
.await?;
|
||||||
|
|
||||||
opentelemetry::global::shutdown_tracer_provider();
|
opentelemetry::global::shutdown_tracer_provider();
|
||||||
|
|
||||||
|
@ -63,9 +66,14 @@ async fn init_app(cfg: &Settings) -> Result<axum::routing::Router> {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let posts = page::load_all(&state, "pages/".into()).await?;
|
let pages = page::load_all(&state, "posts/".into()).await?;
|
||||||
let tags = tag::get_tags(posts.values());
|
info!("{} pages loaded", pages.len());
|
||||||
state.pages = posts;
|
for page in pages.values() {
|
||||||
|
debug!("slug: {}, path: {}", page.slug, page.absolute_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags = tag::get_tags(pages.values());
|
||||||
|
state.pages = pages;
|
||||||
state.tags = tags;
|
state.tags = tags;
|
||||||
let state = Arc::new(state);
|
let state = Arc::new(state);
|
||||||
|
|
||||||
|
@ -81,6 +89,30 @@ async fn init_app(cfg: &Settings) -> Result<axum::routing::Router> {
|
||||||
.with_state(state.clone()))
|
.with_state(state.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn shutdown_signal() {
|
||||||
|
let ctrl_c = async {
|
||||||
|
signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("failed to install Ctrl+C handler");
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
let terminate = async {
|
||||||
|
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||||
|
.expect("failed to install signal handler")
|
||||||
|
.recv()
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let terminate = std::future::pending::<()>();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
() = ctrl_c => {},
|
||||||
|
() = terminate => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum WebsiteError {
|
pub enum WebsiteError {
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
|
@ -16,7 +16,7 @@ static EMAIL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^.+?@\w+(\.\w+)*$").unw
|
||||||
|
|
||||||
pub struct RenderResult {
|
pub struct RenderResult {
|
||||||
pub content_html: String,
|
pub content_html: String,
|
||||||
pub metadata: String
|
pub metadata: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(markdown))]
|
#[instrument(skip(markdown))]
|
||||||
|
|
|
@ -9,7 +9,10 @@ use axum::{
|
||||||
use opentelemetry::{global, KeyValue};
|
use opentelemetry::{global, KeyValue};
|
||||||
use opentelemetry_otlp::WithExportConfig;
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
use opentelemetry_sdk::{
|
use opentelemetry_sdk::{
|
||||||
metrics::reader::{DefaultAggregationSelector, DefaultTemporalitySelector}, propagation::TraceContextPropagator, trace::{RandomIdGenerator, Sampler}, Resource
|
metrics::reader::{DefaultAggregationSelector, DefaultTemporalitySelector},
|
||||||
|
propagation::TraceContextPropagator,
|
||||||
|
trace::{RandomIdGenerator, Sampler},
|
||||||
|
Resource,
|
||||||
};
|
};
|
||||||
use tracing::{field::Empty, info_span, Span};
|
use tracing::{field::Empty, info_span, Span};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
@ -21,8 +24,6 @@ pub fn init(cfg: &Settings) -> Result<(), Error> {
|
||||||
.with_default_directive("info".parse()?)
|
.with_default_directive("info".parse()?)
|
||||||
.parse_lossy(&cfg.logging);
|
.parse_lossy(&cfg.logging);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if cfg.otlp.enabled {
|
if cfg.otlp.enabled {
|
||||||
let tracer = opentelemetry_otlp::new_pipeline()
|
let tracer = opentelemetry_otlp::new_pipeline()
|
||||||
.tracing()
|
.tracing()
|
||||||
|
@ -42,7 +43,6 @@ pub fn init(cfg: &Settings) -> Result<(), Error> {
|
||||||
global::set_text_map_propagator(TraceContextPropagator::new());
|
global::set_text_map_propagator(TraceContextPropagator::new());
|
||||||
let otel_tracer = tracing_opentelemetry::layer().with_tracer(tracer);
|
let otel_tracer = tracing_opentelemetry::layer().with_tracer(tracer);
|
||||||
|
|
||||||
|
|
||||||
let meter = opentelemetry_otlp::new_pipeline()
|
let meter = opentelemetry_otlp::new_pipeline()
|
||||||
.metrics(opentelemetry_sdk::runtime::Tokio)
|
.metrics(opentelemetry_sdk::runtime::Tokio)
|
||||||
.with_exporter(
|
.with_exporter(
|
||||||
|
@ -59,8 +59,6 @@ pub fn init(cfg: &Settings) -> Result<(), Error> {
|
||||||
|
|
||||||
global::set_meter_provider(meter);
|
global::set_meter_provider(meter);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// let logger = opentelemetry_otlp::new_pipeline()
|
// let logger = opentelemetry_otlp::new_pipeline()
|
||||||
// .logging()
|
// .logging()
|
||||||
// .with_exporter(
|
// .with_exporter(
|
||||||
|
@ -76,17 +74,13 @@ pub fn init(cfg: &Settings) -> Result<(), Error> {
|
||||||
.with(otel_tracer)
|
.with(otel_tracer)
|
||||||
.with(tracing_subscriber::fmt::layer().compact())
|
.with(tracing_subscriber::fmt::layer().compact())
|
||||||
.init();
|
.init();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(filter)
|
.with(filter)
|
||||||
.with(tracing_subscriber::fmt::layer().compact())
|
.with(tracing_subscriber::fmt::layer().compact())
|
||||||
.init();
|
.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
src/page.rs
43
src/page.rs
|
@ -1,4 +1,9 @@
|
||||||
use std::{collections::HashMap, fmt::Debug, path::{Path, PathBuf}};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fmt::Debug,
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
@ -7,10 +12,7 @@ use chrono::{DateTime, FixedOffset};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
use tracing::{
|
use tracing::{instrument, log::debug};
|
||||||
instrument,
|
|
||||||
log::debug,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{helpers, markdown, AppState, WebsiteError};
|
use crate::{helpers, markdown, AppState, WebsiteError};
|
||||||
|
|
||||||
|
@ -35,13 +37,19 @@ pub struct Page {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
pub absolute_path: String,
|
pub absolute_path: String,
|
||||||
|
pub etag: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
pub fn new(slug: String, content: String, fm: TomlFrontMatter) -> Page {
|
pub fn new(slug: String, content: String, fm: TomlFrontMatter) -> Page {
|
||||||
|
let mut hasher = std::hash::DefaultHasher::default();
|
||||||
|
content.hash(&mut hasher);
|
||||||
|
let etag = format!("W/\"{:x}\"", hasher.finish());
|
||||||
|
|
||||||
Page {
|
Page {
|
||||||
absolute_path: format!("/posts/{slug}/"),
|
absolute_path: format!("/posts/{slug}/"),
|
||||||
slug,
|
slug,
|
||||||
|
etag,
|
||||||
content,
|
content,
|
||||||
title: fm.title,
|
title: fm.title,
|
||||||
draft: fm.draft.unwrap_or(false),
|
draft: fm.draft.unwrap_or(false),
|
||||||
|
@ -81,8 +89,7 @@ pub async fn load_all(state: &AppState, folder: PathBuf) -> Result<HashMap<Strin
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
dirs.push(path);
|
dirs.push(path);
|
||||||
}
|
} else if let Some(ext) = path.extension() {
|
||||||
else if let Some(ext) = path.extension() {
|
|
||||||
if ext == "md" {
|
if ext == "md" {
|
||||||
// it's a page to load
|
// it's a page to load
|
||||||
let page = load_page(state, &path).await?;
|
let page = load_page(state, &path).await?;
|
||||||
|
@ -105,23 +112,27 @@ pub async fn load_all(state: &AppState, folder: PathBuf) -> Result<HashMap<Strin
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn load_page(state: &AppState, path: &Path) -> Result<Page> {
|
pub async fn load_page(state: &AppState, path: &Path) -> Result<Page> {
|
||||||
debug!("loading post: {path:?}");
|
debug!("loading page: {path:?}");
|
||||||
|
|
||||||
let content = fs::read_to_string(path).await?;
|
let content = fs::read_to_string(path).await?;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let path_str = path.to_string_lossy().replace('\\', "/");
|
let path_str = path.to_string_lossy().replace('\\', "/");
|
||||||
|
|
||||||
let slug = path_str
|
let slug = path_str
|
||||||
.trim_start_matches("posts/")
|
.trim_start_matches("posts")
|
||||||
.trim_start_matches('/')
|
.trim_start_matches('/')
|
||||||
.trim_end_matches(".html")
|
.trim_end_matches(".html")
|
||||||
.trim_end_matches(".md")
|
.trim_end_matches(".md")
|
||||||
.trim_end_matches("index")
|
.trim_end_matches("index")
|
||||||
.trim_end_matches('/');
|
.trim_end_matches('/');
|
||||||
|
|
||||||
let base_uri = helpers::uri_with_path(&state.base_url, &format!("/{slug}/"));
|
let base_path = if let Some(i) = path_str.rfind('/') {
|
||||||
|
&path_str[..=i]
|
||||||
|
} else {
|
||||||
|
&path_str
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_uri = helpers::uri_with_path(&state.base_url, base_path);
|
||||||
|
|
||||||
let content = markdown::render_markdown_to_html(Some(&base_uri), &content);
|
let content = markdown::render_markdown_to_html(Some(&base_uri), &content);
|
||||||
|
|
||||||
|
@ -132,10 +143,10 @@ pub async fn load_page(state: &AppState, path: &Path) -> Result<Page> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state, post))]
|
#[instrument(skip(state, page))]
|
||||||
pub async fn render_post(state: &AppState, post: &Page) -> Result<String, WebsiteError> {
|
pub async fn render_page(state: &AppState, page: &Page) -> Result<String, WebsiteError> {
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("page", &post);
|
ctx.insert("page", &page);
|
||||||
ctx.insert("base_url", &state.base_url.to_string());
|
ctx.insert("base_url", &state.base_url.to_string());
|
||||||
|
|
||||||
state
|
state
|
||||||
|
@ -160,7 +171,7 @@ mod tests {
|
||||||
|
|
||||||
state.pages = super::load_all(&state, "posts/".into()).await.unwrap();
|
state.pages = super::load_all(&state, "posts/".into()).await.unwrap();
|
||||||
for post in state.pages.values() {
|
for post in state.pages.values() {
|
||||||
super::render_post(&state, post).await.unwrap();
|
super::render_page(&state, post).await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue