Skip to main content

systemprompt_api/services/static_content/
vite.rs

1use axum::extract::State;
2use axum::http::{header, StatusCode, Uri};
3use axum::response::IntoResponse;
4use std::sync::Arc;
5
6use super::config::StaticContentMatcher;
7use systemprompt_content::ContentRepository;
8use systemprompt_files::FilesConfig;
9use systemprompt_models::{AppPaths, RouteClassifier, RouteType};
10use systemprompt_runtime::AppContext;
11
12#[derive(Clone, Debug)]
13pub struct StaticContentState {
14    pub ctx: Arc<AppContext>,
15    pub matcher: Arc<StaticContentMatcher>,
16    pub route_classifier: Arc<RouteClassifier>,
17}
18
19pub async fn serve_static_content(
20    State(state): State<StaticContentState>,
21    uri: Uri,
22    req_ctx: Option<axum::Extension<systemprompt_models::RequestContext>>,
23) -> impl IntoResponse {
24    let matcher = state.matcher;
25    let dist_dir = match AppPaths::get() {
26        Ok(paths) => paths.web().dist().to_path_buf(),
27        Err(_) => {
28            return (
29                StatusCode::INTERNAL_SERVER_ERROR,
30                "AppPaths not initialized",
31            )
32                .into_response();
33        },
34    };
35
36    let path = uri.path();
37
38    if matches!(
39        state.route_classifier.classify(path, "GET"),
40        RouteType::StaticAsset { .. }
41    ) {
42        let files_config = match FilesConfig::get() {
43            Ok(config) => config,
44            Err(_) => {
45                return (
46                    StatusCode::INTERNAL_SERVER_ERROR,
47                    "FilesConfig not initialized",
48                )
49                    .into_response();
50            },
51        };
52        let files_prefix = format!("{}/", files_config.url_prefix());
53        let asset_path = if let Some(relative_path) = path.strip_prefix(&files_prefix) {
54            files_config.files().join(relative_path)
55        } else {
56            let trimmed_path = path.trim_start_matches('/');
57            dist_dir.join(trimmed_path)
58        };
59
60        if asset_path.exists() && asset_path.is_file() {
61            match std::fs::read(&asset_path) {
62                Ok(content) => {
63                    let mime_type = match asset_path.extension().and_then(|ext| ext.to_str()) {
64                        Some("js") => "application/javascript",
65                        Some("css") => "text/css",
66                        Some("woff" | "woff2") => "font/woff2",
67                        Some("ttf") => "font/ttf",
68                        Some("png") => "image/png",
69                        Some("jpg" | "jpeg") => "image/jpeg",
70                        Some("svg") => "image/svg+xml",
71                        Some("ico") => "image/x-icon",
72                        Some("json") => "application/json",
73                        _ => "application/octet-stream",
74                    };
75
76                    return (StatusCode::OK, [(header::CONTENT_TYPE, mime_type)], content)
77                        .into_response();
78                },
79                Err(_) => {
80                    return (StatusCode::INTERNAL_SERVER_ERROR, "Error reading asset")
81                        .into_response();
82                },
83            }
84        }
85        return (StatusCode::NOT_FOUND, "Asset not found").into_response();
86    }
87
88    if path == "/" {
89        let index_path = dist_dir.join("index.html");
90        if index_path.exists() {
91            match std::fs::read(&index_path) {
92                Ok(content) => {
93                    return (
94                        StatusCode::OK,
95                        [(header::CONTENT_TYPE, "text/html")],
96                        content,
97                    )
98                        .into_response();
99                },
100                Err(_) => {
101                    return (
102                        StatusCode::INTERNAL_SERVER_ERROR,
103                        "Error reading index.html",
104                    )
105                        .into_response();
106                },
107            }
108        }
109        return (StatusCode::NOT_FOUND, "Homepage not found").into_response();
110    }
111
112    if path == "/sitemap.xml" || path == "/robots.txt" || path == "/llms.txt" || path == "/feed.xml"
113    {
114        let trimmed_path = path.trim_start_matches('/');
115        let file_path = dist_dir.join(trimmed_path);
116        if file_path.exists() {
117            match std::fs::read(&file_path) {
118                Ok(content) => {
119                    let mime_type = if path == "/feed.xml" {
120                        "application/rss+xml; charset=utf-8"
121                    } else {
122                        match file_path.extension().and_then(|ext| ext.to_str()) {
123                            Some("xml") => "application/xml",
124                            _ => "text/plain",
125                        }
126                    };
127                    return (StatusCode::OK, [(header::CONTENT_TYPE, mime_type)], content)
128                        .into_response();
129                },
130                Err(_) => {
131                    return (StatusCode::INTERNAL_SERVER_ERROR, "Error reading file")
132                        .into_response();
133                },
134            }
135        }
136        return (StatusCode::NOT_FOUND, "File not found").into_response();
137    }
138
139    let trimmed_path = path.trim_start_matches('/');
140    let parent_route_path = dist_dir.join(trimmed_path).join("index.html");
141    if parent_route_path.exists() {
142        match std::fs::read(&parent_route_path) {
143            Ok(content) => {
144                return (
145                    StatusCode::OK,
146                    [(header::CONTENT_TYPE, "text/html")],
147                    content,
148                )
149                    .into_response();
150            },
151            Err(_) => {
152                return (
153                    StatusCode::INTERNAL_SERVER_ERROR,
154                    "Error reading parent route",
155                )
156                    .into_response();
157            },
158        }
159    }
160
161    if let Some((slug, source_id)) = matcher.matches(path) {
162        let exact_path = dist_dir.join(trimmed_path);
163        if exact_path.exists() && exact_path.is_file() {
164            return serve_html_with_analytics(
165                &exact_path,
166                &slug,
167                &source_id,
168                req_ctx.as_ref().map(|ext| ext.0.clone()),
169            )
170            .into_response();
171        }
172
173        let index_path = dist_dir.join(trimmed_path).join("index.html");
174        if index_path.exists() {
175            return serve_html_with_analytics(
176                &index_path,
177                &slug,
178                &source_id,
179                req_ctx.as_ref().map(|ext| ext.0.clone()),
180            )
181            .into_response();
182        }
183
184        let content_repo = match ContentRepository::new(state.ctx.db_pool()) {
185            Ok(r) => r,
186            Err(_) => {
187                return (
188                    StatusCode::INTERNAL_SERVER_ERROR,
189                    axum::response::Html("Database connection error"),
190                )
191                    .into_response();
192            },
193        };
194        match content_repo.get_by_slug(&slug).await {
195            Ok(Some(_)) => {
196                return (
197                    StatusCode::INTERNAL_SERVER_ERROR,
198                    axum::response::Html(format!(
199                        r#"<!DOCTYPE html>
200<html>
201<head>
202    <title>Content Not Prerendered</title>
203    <meta charset="utf-8">
204    <meta name="viewport" content="width=device-width, initial-scale=1">
205    <style>
206        body {{ font-family: system-ui, sans-serif; max-width: 600px; margin: 100px auto; padding: 20px; }}
207        h1 {{ color: #d32f2f; }}
208        code {{ background: #f5f5f5; padding: 2px 6px; border-radius: 3px; }}
209    </style>
210</head>
211<body>
212    <h1>Content Not Prerendered</h1>
213    <p>The content exists in the database but has not been prerendered to HTML.</p>
214    <p>Route: <code>{}</code></p>
215    <p>Slug: <code>{}</code></p>
216    <p>Run the prerendering build step to generate static HTML.</p>
217</body>
218</html>"#,
219                        path, slug
220                    ))
221                ).into_response();
222            },
223            Ok(None) => {
224                return (
225                    StatusCode::NOT_FOUND,
226                    axum::response::Html(r#"<!DOCTYPE html>
227<html>
228<head>
229    <title>404 Not Found</title>
230    <meta charset="utf-8">
231    <meta name="viewport" content="width=device-width, initial-scale=1">
232    <style>
233        body { font-family: system-ui, sans-serif; max-width: 600px; margin: 100px auto; padding: 20px; }
234        h1 { color: #333; }
235        a { color: #1976d2; text-decoration: none; }
236        a:hover { text-decoration: underline; }
237    </style>
238</head>
239<body>
240    <h1>404 - Page Not Found</h1>
241    <p>The page you're looking for doesn't exist.</p>
242    <p><a href="/">← Back to home</a></p>
243</body>
244</html>"#.to_string())
245                ).into_response();
246            },
247            Err(e) => {
248                tracing::error!(error = %e, "Database error checking content");
249                return (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
250                    .into_response();
251            },
252        }
253    }
254
255    (
256        StatusCode::NOT_FOUND,
257        axum::response::Html(r#"<!DOCTYPE html>
258<html>
259<head>
260    <title>404 Not Found</title>
261    <meta charset="utf-8">
262    <meta name="viewport" content="width=device-width, initial-scale=1">
263    <style>
264        body { font-family: system-ui, sans-serif; max-width: 600px; margin: 100px auto; padding: 20px; }
265        h1 { color: #333; }
266        a { color: #1976d2; text-decoration: none; }
267        a:hover { text-decoration: underline; }
268    </style>
269</head>
270<body>
271    <h1>404 - Page Not Found</h1>
272    <p>The page you're looking for doesn't exist.</p>
273    <p><a href="/">← Back to home</a></p>
274</body>
275</html>"#)
276    ).into_response()
277}
278
279fn serve_html_with_analytics(
280    html_path: &std::path::Path,
281    _slug: &str,
282    _source_id: &str,
283    _req_ctx: Option<systemprompt_models::RequestContext>,
284) -> impl IntoResponse {
285    let Ok(html_content) = std::fs::read(html_path) else {
286        return (StatusCode::INTERNAL_SERVER_ERROR, "Error reading file").into_response();
287    };
288
289    let mut response = (StatusCode::OK, html_content).into_response();
290    response.headers_mut().insert(
291        header::CONTENT_TYPE,
292        http::HeaderValue::from_static("text/html"),
293    );
294
295    response
296}