Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added crates/modelrelay-cloud/assets/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions crates/modelrelay-cloud/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub fn router(state: Arc<CloudState>) -> Router {
.route("/robots.txt", get(robots_txt))
.route("/sitemap.xml", get(sitemap_xml))
.route("/favicon.ico", get(favicon_ico))
.route("/og-image.png", get(og_image))
.fallback(not_found)
.layer(middleware::from_fn(security_headers))
.layer(middleware::from_fn(csrf::csrf_middleware))
Expand Down Expand Up @@ -135,6 +136,26 @@ async fn favicon_ico() -> impl IntoResponse {
([(axum::http::header::CONTENT_TYPE, "image/svg+xml")], svg)
}

/// 1200x630 branded social-share card used by `og:image` / `twitter:image`.
///
/// Baked into the binary so it ships with the image everywhere the cloud
/// service runs and so the site's CSP (`img-src 'self' data:`) can serve it
/// same-origin without special-casing an external host.
static OG_IMAGE_BYTES: &[u8] = include_bytes!("../../assets/og-image.png");

async fn og_image() -> impl IntoResponse {
(
[
(axum::http::header::CONTENT_TYPE, "image/png"),
(
axum::http::header::CACHE_CONTROL,
"public, max-age=86400, immutable",
),
],
OG_IMAGE_BYTES,
)
}

/// Routes that do not require a session.
const SESSION_EXEMPT_ROUTES: &[&str] = &[
"/",
Expand All @@ -143,6 +164,7 @@ const SESSION_EXEMPT_ROUTES: &[&str] = &[
"/robots.txt",
"/sitemap.xml",
"/favicon.ico",
"/og-image.png",
"/checkout/cancel",
"/terms",
"/privacy",
Expand Down
8 changes: 7 additions & 1 deletion crates/modelrelay-cloud/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@
<meta property="og:description" content="Route inference to your own GPU workers through a secure relay. OpenAI, Anthropic, and Responses API compatible.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://modelrelay.io/">
<meta property="og:image" content="https://modelrelay.io/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="ModelRelay — Managed LLM Relay for Your AI Workers">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="ModelRelay — Managed LLM Relay for Your AI Workers">
<meta name="twitter:description" content="Route inference to your own GPU workers through a secure relay. OpenAI, Anthropic, and Responses API compatible.">
<meta name="twitter:image" content="https://modelrelay.io/og-image.png">
<meta name="twitter:image:alt" content="ModelRelay — Managed LLM Relay for Your AI Workers">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%237c3aed'/%3E%3Cpath d='M8 10h4l4 6-4 6H8l4-6-4-6zm12 0h4l4 6-4 6h-4l4-6-4-6z' fill='white'/%3E%3C/svg%3E">
<style>
Expand Down
8 changes: 7 additions & 1 deletion crates/modelrelay-cloud/templates/pricing.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
<meta property="og:title" content="Pricing — ModelRelay">
<meta property="og:description" content="Simple, transparent pricing for ModelRelay. $20/month flat rate — unlimited workers, unlimited requests, no per-request fees.">
<meta property="og:type" content="website">
<meta property="og:image" content="https://modelrelay.io/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="ModelRelay — Managed LLM Relay for Your AI Workers">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Pricing — ModelRelay">
<meta name="twitter:description" content="Simple, transparent pricing for ModelRelay. $20/month flat rate — unlimited workers, unlimited requests, no per-request fees.">
<meta name="twitter:image" content="https://modelrelay.io/og-image.png">
<meta name="twitter:image:alt" content="ModelRelay — Managed LLM Relay for Your AI Workers">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%237c3aed'/%3E%3Cpath d='M8 10h4l4 6-4 6H8l4-6-4-6zm12 0h4l4 6-4 6h-4l4-6-4-6z' fill='white'/%3E%3C/svg%3E">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
Expand Down
71 changes: 69 additions & 2 deletions crates/modelrelay-cloud/tests/http_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,73 @@ async fn favicon_returns_success() {
assert!(body.contains("<svg"), "expected SVG favicon");
}

#[tokio::test]
async fn og_image_returns_png() {
let resp = app()
.oneshot(
Request::builder()
.uri("/og-image.png")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);

let content_type = resp
.headers()
.get(axum::http::header::CONTENT_TYPE)
.expect("og-image should set Content-Type")
.to_str()
.expect("Content-Type should be valid UTF-8");
assert!(
content_type.starts_with("image/png"),
"expected image/png, got {content_type}"
);

let cache_control = resp
.headers()
.get(axum::http::header::CACHE_CONTROL)
.expect("og-image should set Cache-Control")
.to_str()
.expect("Cache-Control should be valid UTF-8");
assert!(
cache_control.contains("max-age"),
"expected Cache-Control with max-age, got {cache_control}"
);

let body = resp.into_body().collect().await.unwrap().to_bytes();
assert!(
body.len() > 1000,
"og-image should be a real PNG, got {} bytes",
body.len()
);
// PNG magic bytes: 89 50 4E 47 0D 0A 1A 0A
assert_eq!(
&body[..8],
&[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
"body should start with PNG magic bytes"
);
}

#[tokio::test]
async fn landing_page_includes_og_image_meta() {
let (status, body) = get("/").await;
assert_eq!(status, StatusCode::OK);
assert!(
body.contains(r#"property="og:image" content="https://modelrelay.io/og-image.png""#),
"landing page should include og:image meta tag"
);
assert!(
body.contains(r#"name="twitter:card" content="summary_large_image""#),
"landing page should use summary_large_image twitter card"
);
assert!(
body.contains(r#"name="twitter:image" content="https://modelrelay.io/og-image.png""#),
"landing page should include twitter:image meta tag"
);
}

// ─── Stripe webhook ────────────────────────────────────────────────────────

#[tokio::test]
Expand Down Expand Up @@ -581,8 +648,8 @@ async fn pricing_page_has_og_url_and_canonical() {
"/pricing should have og:type"
);
assert!(
body.contains(r#"<meta name="twitter:card" content="summary""#),
"/pricing should have twitter:card"
body.contains(r#"<meta name="twitter:card" content="summary_large_image""#),
"/pricing should have twitter:card set to summary_large_image"
);
assert!(
body.contains(r#"<meta name="twitter:title" content="Pricing — ModelRelay""#),
Expand Down
8 changes: 7 additions & 1 deletion crates/modelrelay-web/src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2735,10 +2735,16 @@ pub fn page_shell_custom(
<meta property="og:description" content="Route inference to your own GPU workers through a secure relay. OpenAI, Anthropic, and Responses API compatible.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://modelrelay.io{path}">
<meta property="og:image" content="https://modelrelay.io/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="ModelRelay — Managed LLM Relay for Your AI Workers">
<link rel="canonical" href="https://modelrelay.io{path}">
<meta name="twitter:card" content="summary">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{title} — ModelRelay">
<meta name="twitter:description" content="Route inference to your own GPU workers through a secure relay. OpenAI, Anthropic, and Responses API compatible.">
<meta name="twitter:image" content="https://modelrelay.io/og-image.png">
<meta name="twitter:image:alt" content="ModelRelay — Managed LLM Relay for Your AI Workers">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%237c3aed'/><text x='50' y='72' font-size='60' font-weight='bold' text-anchor='middle' fill='white'>M</text></svg>">
<style>
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
Expand Down