8
8
//! likely to be removed in the future.
9
9
10
10
use std:: borrow:: Cow ;
11
- use std:: path :: Path ;
11
+ use std:: ops :: Not ;
12
12
use std:: sync:: { Arc , OnceLock } ;
13
13
14
14
use axum:: extract:: Request ;
@@ -18,6 +18,7 @@ use futures_util::future::{BoxFuture, Shared};
18
18
use futures_util:: FutureExt ;
19
19
use http:: { header, HeaderMap , HeaderValue , Method , StatusCode } ;
20
20
use minijinja:: { context, Environment } ;
21
+ use url:: Url ;
21
22
22
23
use crate :: app:: AppState ;
23
24
@@ -73,24 +74,11 @@ pub async fn serve_html(state: AppState, request: Request, next: Next) -> Respon
73
74
return ( StatusCode :: METHOD_NOT_ALLOWED , headers) . into_response ( ) ;
74
75
}
75
76
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) ;
94
82
95
83
// Fetch the HTML from cache given `og_image_url` as key or render it
96
84
let html = RENDERED_HTML_CACHE
@@ -120,11 +108,7 @@ pub async fn serve_html(state: AppState, request: Request, next: Next) -> Respon
120
108
. await ;
121
109
122
110
// 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 ( )
128
112
} else {
129
113
// Return a 404 to crawlers that don't send `Accept: text/hml`.
130
114
// 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
133
117
StatusCode :: NOT_FOUND . into_response ( )
134
118
}
135
119
}
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