88//! likely to be removed in the future.
99
1010use std:: borrow:: Cow ;
11- use std:: path :: Path ;
11+ use std:: ops :: Not ;
1212use std:: sync:: { Arc , OnceLock } ;
1313
1414use axum:: extract:: Request ;
@@ -18,6 +18,7 @@ use futures_util::future::{BoxFuture, Shared};
1818use futures_util:: FutureExt ;
1919use http:: { header, HeaderMap , HeaderValue , Method , StatusCode } ;
2020use minijinja:: { context, Environment } ;
21+ use url:: Url ;
2122
2223use crate :: app:: AppState ;
2324
@@ -73,24 +74,11 @@ pub async fn serve_html(state: AppState, request: Request, next: Next) -> Respon
7374 return ( StatusCode :: METHOD_NOT_ALLOWED , headers) . into_response ( ) ;
7475 }
7576
76- // Come up with an Open Graph image URL. In case a crate page is requested,
77- // we use the crate's name and the OG image base URL from config to
78- // generate one, otherwise we use the fallback image.
79- let og_image_url = ' og: {
80- if let Some ( suffix) = path. strip_prefix ( PATH_PREFIX_CRATES ) {
81- let len = suffix. find ( '/' ) . unwrap_or ( suffix. len ( ) ) ;
82- let krate = & suffix[ ..len] ;
83-
84- // `state.config.og_image_base_url` will always be `Some` as that's required
85- // if `state.config.index_html_template_path` is `Some`, and otherwise this
86- // middleware won't be executed; see `crate::middleware::apply_axum_middleware`.
87- if let Ok ( og_img_url) = state. config . og_image_base_url . as_ref ( ) . unwrap ( ) . join ( krate)
88- {
89- break ' og Cow :: from ( og_img_url. to_string ( ) ) ;
90- }
91- }
92- OG_IMAGE_FALLBACK_URL . into ( )
93- } ;
77+ // `state.config.og_image_base_url` will always be `Some` as that's required
78+ // if `state.config.index_html_template_path` is `Some`, and otherwise this
79+ // middleware won't be executed; see `crate::middleware::apply_axum_middleware`.
80+ let og_image_base_url = state. config . og_image_base_url . as_ref ( ) . unwrap ( ) ;
81+ let og_image_url = generate_og_image_url ( path, og_image_base_url) ;
9482
9583 // Fetch the HTML from cache given `og_image_url` as key or render it
9684 let html = RENDERED_HTML_CACHE
@@ -120,11 +108,7 @@ pub async fn serve_html(state: AppState, request: Request, next: Next) -> Respon
120108 . await ;
121109
122110 // Serve static Ember page to bootstrap the frontend
123- Response :: builder ( )
124- . header ( header:: CONTENT_TYPE , "text/html" )
125- . header ( header:: CONTENT_LENGTH , html. len ( ) )
126- . body ( axum:: body:: Body :: new ( html) )
127- . unwrap_or_else ( |_| StatusCode :: INTERNAL_SERVER_ERROR . into_response ( ) )
111+ axum:: response:: Html ( html) . into_response ( )
128112 } else {
129113 // Return a 404 to crawlers that don't send `Accept: text/hml`.
130114 // This is to preserve legacy behavior and will likely change.
@@ -133,3 +117,84 @@ pub async fn serve_html(state: AppState, request: Request, next: Next) -> Respon
133117 StatusCode :: NOT_FOUND . into_response ( )
134118 }
135119}
120+
121+ /// Extract the crate name from the path, by stripping [`PATH_PREFIX_CRATES`]
122+ /// prefix, and returning the firsts path segment from the result.
123+ /// Returns `None` if the path was not prefixed with [`PATH_PREFIX_CRATES`].
124+ fn extract_crate_name ( path : & str ) -> Option < & str > {
125+ path. strip_prefix ( PATH_PREFIX_CRATES ) . and_then ( |suffix| {
126+ let len = suffix. find ( '/' ) . unwrap_or ( suffix. len ( ) ) ;
127+ let krate = & suffix[ ..len] ;
128+ krate. is_empty ( ) . not ( ) . then_some ( krate)
129+ } )
130+ }
131+
132+ /// Come up with an Open Graph image URL. In case a crate page is requested,
133+ /// we use the crate's name as extracted from the request path and the OG image
134+ /// base URL from config to generate one, otherwise we use the fallback image.
135+ fn generate_og_image_url ( path : & str , og_image_base_url : & Url ) -> Cow < ' static , str > {
136+ if let Some ( krate) = extract_crate_name ( path) {
137+ if let Ok ( og_img_url) = og_image_base_url
138+ . join ( krate)
139+ . map ( |url_without_extrrension| format ! ( "{url_without_extrrension}.png" ) )
140+ {
141+ return og_img_url. into ( ) ;
142+ }
143+ }
144+
145+ OG_IMAGE_FALLBACK_URL . into ( )
146+ }
147+
148+ #[ cfg( test) ]
149+ mod tests {
150+ use googletest:: { assert_that, prelude:: eq} ;
151+ use url:: Url ;
152+
153+ use crate :: middleware:: ember_html:: {
154+ extract_crate_name, generate_og_image_url, OG_IMAGE_FALLBACK_URL ,
155+ } ;
156+
157+ #[ test]
158+ fn test_extract_crate_name ( ) {
159+ const PATHS : & [ ( & str , Option < & str > ) ] = & [
160+ ( "/crates/tokio" , Some ( "tokio" ) ) ,
161+ ( "/crates/tokio/versions" , Some ( "tokio" ) ) ,
162+ ( "/crates/tokio/" , Some ( "tokio" ) ) ,
163+ ( "/" , None ) ,
164+ ( "/crates" , None ) ,
165+ ( "/crates/" , None ) ,
166+ ( "/dashboard/" , None ) ,
167+ ( "/settings/profile" , None ) ,
168+ ] ;
169+
170+ for ( path, expected) in PATHS . iter ( ) . copied ( ) {
171+ assert_that ! ( extract_crate_name( path) , eq( expected) ) ;
172+ }
173+ }
174+
175+ #[ test]
176+ fn test_generate_og_image_url ( ) {
177+ const PATHS : & [ ( & str , & str ) ] = & [
178+ ( "/crates/tokio" , "http://localhost:3000/og/tokio.png" ) ,
179+ (
180+ "/crates/tokio/versions" ,
181+ "http://localhost:3000/og/tokio.png" ,
182+ ) ,
183+ ( "/crates/tokio/" , "http://localhost:3000/og/tokio.png" ) ,
184+ ( "/" , OG_IMAGE_FALLBACK_URL ) ,
185+ ( "/crates" , OG_IMAGE_FALLBACK_URL ) ,
186+ ( "/crates/" , OG_IMAGE_FALLBACK_URL ) ,
187+ ( "/dashboard/" , OG_IMAGE_FALLBACK_URL ) ,
188+ ( "/settings/profile" , OG_IMAGE_FALLBACK_URL ) ,
189+ ] ;
190+
191+ let og_image_base_url: Url = "http://localhost:3000/og/" . parse ( ) . unwrap ( ) ;
192+
193+ for ( path, expected) in PATHS . iter ( ) . copied ( ) {
194+ assert_that ! (
195+ generate_og_image_url( path, & og_image_base_url) ,
196+ eq( expected)
197+ ) ;
198+ }
199+ }
200+ }
0 commit comments