77//! For now, there is an additional check to see if the `Accept` header contains "html". This is
88//! likely to be removed in the future.
99
10+ use std:: borrow:: Cow ;
11+ use std:: path:: Path ;
12+ use std:: sync:: OnceLock ;
13+
1014use axum:: extract:: Request ;
1115use axum:: middleware:: Next ;
1216use axum:: response:: { IntoResponse , Response } ;
13- use http:: { header, StatusCode } ;
14- use tower:: ServiceExt ;
15- use tower_http:: services:: ServeFile ;
17+ use futures_util:: future:: { BoxFuture , Shared } ;
18+ use futures_util:: FutureExt ;
19+ use http:: { header, HeaderMap , HeaderValue , Method , StatusCode } ;
20+ use minijinja:: { context, Environment } ;
1621
17- pub async fn serve_html ( request : Request , next : Next ) -> Response {
18- let path = & request. uri ( ) . path ( ) ;
22+ use crate :: app:: AppState ;
23+
24+ const OG_IMAGE_FALLBACK_URL : & str = "https://crates.io/assets/og-image.png" ;
25+ const INDEX_TEMPLATE_NAME : & str = "index_html" ;
26+ const PATH_PREFIX_CRATES : & str = "/crates/" ;
27+
28+ type TemplateEnvFut = Shared < BoxFuture < ' static , minijinja:: Environment < ' static > > > ;
29+
30+ /// Initialize [`minijinja::Environment`] given the path to the index.html file. This should
31+ /// only be done once as it will load said file from persistent storage.
32+ async fn init_template_env (
33+ index_html_template_path : impl AsRef < Path > ,
34+ ) -> minijinja:: Environment < ' static > {
35+ let template_j2 = tokio:: fs:: read_to_string ( index_html_template_path. as_ref ( ) )
36+ . await
37+ . expect ( "Error loading index.html template. Is the frontend package built yet?" ) ;
38+
39+ let mut env = Environment :: empty ( ) ;
40+ env. add_template_owned ( INDEX_TEMPLATE_NAME , template_j2)
41+ . expect ( "Error loading template" ) ;
42+ env
43+ }
44+
45+ pub async fn serve_html ( state : AppState , request : Request , next : Next ) -> Response {
46+ static TEMPLATE_ENV : OnceLock < TemplateEnvFut > = OnceLock :: new ( ) ;
1947
48+ let path = & request. uri ( ) . path ( ) ;
2049 // The "/git/" prefix is only used in development (when within a docker container)
2150 if path. starts_with ( "/api/" ) || path. starts_with ( "/git/" ) {
2251 next. run ( request) . await
@@ -26,12 +55,58 @@ pub async fn serve_html(request: Request, next: Next) -> Response {
2655 . iter ( )
2756 . any ( |val| val. to_str ( ) . unwrap_or_default ( ) . contains ( "html" ) )
2857 {
58+ if !matches ! ( * request. method( ) , Method :: HEAD | Method :: GET ) {
59+ let headers =
60+ HeaderMap :: from_iter ( [ ( header:: ALLOW , HeaderValue :: from_static ( "GET,HEAD" ) ) ] ) ;
61+ return ( StatusCode :: METHOD_NOT_ALLOWED , headers) . into_response ( ) ;
62+ }
63+
64+ // Come up with an Open Graph image URL. In case a crate page is requested,
65+ // we use the crate's name and the OG image base URL from config to
66+ // generate one, otherwise we use the fallback image.
67+ let og_image_url: Cow < ' _ , _ > = if let Some ( suffix) = path. strip_prefix ( PATH_PREFIX_CRATES ) {
68+ let len = suffix. find ( '/' ) . unwrap_or ( suffix. len ( ) ) ;
69+ let krate = & suffix[ ..len] ;
70+
71+ // `state.config.og_image_base_url` will always be `Some` as that's required
72+ // if `state.config.index_html_template_path` is `Some`, and otherwise this
73+ // middleware won't be executed; see `crate::middleware::apply_axum_middleware`.
74+ if let Ok ( og_img_url) = state. config . og_image_base_url . as_ref ( ) . unwrap ( ) . join ( krate) {
75+ Cow :: from ( og_img_url. to_string ( ) )
76+ } else {
77+ OG_IMAGE_FALLBACK_URL . into ( )
78+ }
79+ } else {
80+ OG_IMAGE_FALLBACK_URL . into ( )
81+ } ;
82+
83+ // `OnceLock::get_or_init` blocks as long as its intializer is running in another thread.
84+ // Note that this won't take long, as the constructed Futures are not awaited
85+ // during initialization.
86+ let template_env = TEMPLATE_ENV . get_or_init ( || {
87+ // At this point we can safely assume `state.config.index_html_template_path` is `Some`,
88+ // as this middleware won't be executed otherwise; see `crate::middleware::apply_axum_middleware`.
89+ init_template_env ( state. config . index_html_template_path . clone ( ) . unwrap ( ) )
90+ . boxed ( )
91+ . shared ( )
92+ } ) ;
93+
94+ // TODO use moka caching here with og_image_url as key and the rendered html as value
95+
96+ // Render the HTML given the OG image URL
97+ let env = template_env. clone ( ) . await ;
98+ let html = env
99+ . get_template ( INDEX_TEMPLATE_NAME )
100+ . unwrap ( )
101+ . render ( context ! { og_image_url} )
102+ . expect ( "Error rendering index" ) ;
103+
29104 // Serve static Ember page to bootstrap the frontend
30- ServeFile :: new ( "dist/index.html" )
31- . oneshot ( request )
32- . await
33- . map ( |response| response . map ( axum:: body:: Body :: new) )
34- . unwrap_or_else ( |_| StatusCode :: INTERNAL_SERVER_ERROR . into_response ( ) )
105+ Response :: builder ( )
106+ . header ( header :: CONTENT_TYPE , "text/html" )
107+ . header ( header :: CONTENT_LENGTH , html . len ( ) )
108+ . body ( axum:: body:: Body :: new ( html ) )
109+ . unwrap ( )
35110 } else {
36111 // Return a 404 to crawlers that don't send `Accept: text/hml`.
37112 // This is to preserve legacy behavior and will likely change.
0 commit comments