Skip to content

Commit 6aeb3cd

Browse files
committed
Extract url generation and crate name extraction logic from ember_html middleware, add .png extension to generated OG image URL
1 parent 1c92824 commit 6aeb3cd

File tree

1 file changed

+89
-24
lines changed

1 file changed

+89
-24
lines changed

src/middleware/ember_html.rs

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! likely to be removed in the future.
99
1010
use std::borrow::Cow;
11-
use std::path::Path;
11+
use std::ops::Not;
1212
use std::sync::{Arc, OnceLock};
1313

1414
use axum::extract::Request;
@@ -18,6 +18,7 @@ use futures_util::future::{BoxFuture, Shared};
1818
use futures_util::FutureExt;
1919
use http::{header, HeaderMap, HeaderValue, Method, StatusCode};
2020
use minijinja::{context, Environment};
21+
use url::Url;
2122

2223
use 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

Comments
 (0)