106 lines
3.5 KiB
Rust
106 lines
3.5 KiB
Rust
use std::sync::Arc;
|
|
|
|
use axum::body::Body;
|
|
use axum::extract::Request;
|
|
use axum::http::header;
|
|
use axum::middleware::Next;
|
|
use axum::response::Response;
|
|
|
|
use crate::state::AppState;
|
|
|
|
const OG_PLACEHOLDER: &str =
|
|
r#"<meta name="x-og-placeholder" content="__PERFECT_POSTCODE_OG_TAGS__"/>"#;
|
|
|
|
pub async fn og_middleware(request: Request, next: Next) -> Response {
|
|
let path = request.uri().path().to_string();
|
|
// Capture the query string before passing the request through
|
|
let query_string = request.uri().query().unwrap_or("").to_string();
|
|
|
|
// Get state from extensions
|
|
let state = request.extensions().get::<Arc<AppState>>().cloned();
|
|
|
|
let response = next.run(request).await;
|
|
|
|
// Only inject OG tags into SPA HTML responses, not proxied PocketBase responses
|
|
if path.starts_with("/pb/") || path.starts_with("/api/") {
|
|
return response;
|
|
}
|
|
|
|
let content_type = response
|
|
.headers()
|
|
.get(header::CONTENT_TYPE)
|
|
.and_then(|val| val.to_str().ok())
|
|
.unwrap_or("");
|
|
|
|
if !content_type.contains("text/html") {
|
|
return response;
|
|
}
|
|
|
|
let state = match state {
|
|
Some(st) => st,
|
|
None => return response,
|
|
};
|
|
|
|
let index_html = match &state.index_html {
|
|
Some(html) => html,
|
|
None => return response,
|
|
};
|
|
|
|
// Build OG-injected HTML (og=1 triggers heading overlay on screenshot)
|
|
let is_invite = path.starts_with("/invite/");
|
|
|
|
let og_image_url = if is_invite {
|
|
// Include path= so the screenshot service navigates to /invite/CODE
|
|
if query_string.is_empty() {
|
|
format!("{}/api/screenshot?og=1&path={}", state.public_url, path)
|
|
} else {
|
|
format!(
|
|
"{}/api/screenshot?og=1&path={}&{}",
|
|
state.public_url, path, query_string
|
|
)
|
|
}
|
|
} else if query_string.is_empty() {
|
|
format!("{}/api/screenshot?og=1", state.public_url)
|
|
} else {
|
|
format!("{}/api/screenshot?og=1&{}", state.public_url, query_string)
|
|
};
|
|
|
|
let og_url = if query_string.is_empty() {
|
|
format!("{}{}", state.public_url, path)
|
|
} else {
|
|
format!("{}{}?{}", state.public_url, path, query_string)
|
|
};
|
|
|
|
let og_logo = format!("{}/favicon.svg", state.public_url);
|
|
|
|
let (og_title, og_description) = if is_invite {
|
|
(
|
|
"You\u{2019}re invited to Perfect Postcode",
|
|
"Accept your invitation to explore property prices, energy ratings, crime stats, school ratings, and more across England.",
|
|
)
|
|
} else {
|
|
(
|
|
"Perfect Postcode \u{2014} Every neighbourhood in England",
|
|
"Explore property prices, energy ratings, crime stats, school ratings, and more across England on one interactive map.",
|
|
)
|
|
};
|
|
|
|
let og_tags = format!(
|
|
r#"<meta property="og:title" content="{og_title}" />
|
|
<meta property="og:description" content="{og_description}" />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content="{og_url}" />
|
|
<meta property="og:logo" content="{og_logo}" />
|
|
<meta property="og:image" content="{og_image_url}" />
|
|
<meta property="og:image:width" content="1200" />
|
|
<meta property="og:image:height" content="630" />
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:title" content="{og_title}" />
|
|
<meta name="twitter:description" content="{og_description}" />"#
|
|
);
|
|
|
|
let html = index_html.replace(OG_PLACEHOLDER, &og_tags);
|
|
|
|
let (parts, _body) = response.into_parts();
|
|
Response::from_parts(parts, Body::from(html))
|
|
}
|