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
66 changes: 63 additions & 3 deletions crates/modelrelay-cloud/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ 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("/favicon-32.png", get(favicon_32_png))
.route("/apple-touch-icon.png", get(apple_touch_icon))
.route("/icon-512.png", get(icon_512_png))
.route("/og-image.png", get(og_image))
.fallback(not_found)
.layer(middleware::from_fn(security_headers))
Expand Down Expand Up @@ -130,10 +133,64 @@ async fn sitemap_xml() -> impl IntoResponse {
)
}

/// Real multi-size Windows .ico (16x16 + 32x32 PNG sub-images) shipped by the
/// desktop crate. Baked in so the site and desktop app share branding.
static FAVICON_ICO_BYTES: &[u8] = include_bytes!("../../../modelrelay-desktop/icons/icon.ico");
static FAVICON_32_PNG_BYTES: &[u8] = include_bytes!("../../../modelrelay-desktop/icons/32x32.png");
static APPLE_TOUCH_ICON_BYTES: &[u8] =
include_bytes!("../../../modelrelay-desktop/icons/128x128@2x.png");
static ICON_512_PNG_BYTES: &[u8] = include_bytes!("../../../modelrelay-desktop/icons/icon.png");

async fn favicon_ico() -> impl IntoResponse {
// Minimal SVG favicon matching the purple "M" logo used in the HTML head.
let svg = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#7c3aed"/><text x="16" y="24" font-size="22" font-weight="bold" fill="white" text-anchor="middle" font-family="system-ui,sans-serif">M</text></svg>"##;
([(axum::http::header::CONTENT_TYPE, "image/svg+xml")], svg)
(
[
(axum::http::header::CONTENT_TYPE, "image/x-icon"),
(
axum::http::header::CACHE_CONTROL,
"public, max-age=86400, immutable",
),
],
FAVICON_ICO_BYTES,
)
}

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

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

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

/// 1200x630 branded social-share card used by `og:image` / `twitter:image`.
Expand Down Expand Up @@ -164,6 +221,9 @@ const SESSION_EXEMPT_ROUTES: &[&str] = &[
"/robots.txt",
"/sitemap.xml",
"/favicon.ico",
"/favicon-32.png",
"/apple-touch-icon.png",
"/icon-512.png",
"/og-image.png",
"/checkout/cancel",
"/terms",
Expand Down
8 changes: 5 additions & 3 deletions crates/modelrelay-cloud/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ModelRelay — Managed LLM Relay for Your AI Workers</title>
<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>">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="description" content="Run your local GPU workers behind a managed cloud relay. OpenAI, Anthropic, and Responses API compatible. One endpoint, automatic load balancing, zero DevOps.">
<link rel="canonical" href="https://modelrelay.io/">
<!-- Open Graph -->
Expand All @@ -22,8 +25,7 @@
<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">
<!-- Favicon (duplicate links removed; primary set is above) -->
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
Expand Down
5 changes: 4 additions & 1 deletion crates/modelrelay-cloud/templates/pricing.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
<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">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="shortcut icon" href="/favicon.ico">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
Expand Down
60 changes: 57 additions & 3 deletions crates/modelrelay-cloud/tests/http_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,63 @@ async fn desktop_download_unknown_platform_falls_back_to_releases_page() {

#[tokio::test]
async fn favicon_returns_success() {
let (status, body) = get("/favicon.ico").await;
assert_eq!(status, StatusCode::OK);
assert!(body.contains("<svg"), "expected SVG favicon");
let resp = app()
.oneshot(
Request::builder()
.uri("/favicon.ico")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);

let content_type = resp
.headers()
.get(axum::http::header::CONTENT_TYPE)
.expect("favicon should set Content-Type")
.to_str()
.expect("Content-Type should be valid UTF-8");
assert_eq!(content_type, "image/x-icon");

let bytes = resp.into_body().collect().await.unwrap().to_bytes();
// Windows .ico files start with 00 00 01 00 (reserved + type=icon).
assert!(
bytes.len() >= 4 && &bytes[..4] == b"\x00\x00\x01\x00",
"expected a real Windows .ico resource, got {} bytes starting with {:?}",
bytes.len(),
&bytes[..bytes.len().min(8)]
);
}

#[tokio::test]
async fn apple_touch_icon_returns_png() {
let resp = app()
.oneshot(
Request::builder()
.uri("/apple-touch-icon.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("apple-touch-icon should set Content-Type")
.to_str()
.expect("Content-Type should be valid UTF-8");
assert_eq!(content_type, "image/png");

let bytes = resp.into_body().collect().await.unwrap().to_bytes();
// PNG files start with 89 50 4E 47 0D 0A 1A 0A.
assert!(
bytes.len() >= 8 && &bytes[..8] == b"\x89PNG\r\n\x1a\n",
"expected a real PNG, got {} bytes",
bytes.len()
);
}

#[tokio::test]
Expand Down
5 changes: 4 additions & 1 deletion crates/modelrelay-web/src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2745,7 +2745,10 @@ pub fn page_shell_custom(
<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>">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icon-512.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="shortcut icon" href="/favicon.ico">
<style>
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
Expand Down