Skip to content

Commit f8fc579

Browse files
committed
feat: Adds SPA fallback route to server
Implements a SPA fallback route that serves index.html for unmatched routes. This enables client-side routing for dynamic routes when using embedded static files. Excludes API routes from the fallback to ensure they are handled by dedicated handlers. Also includes the required headers by pyodide (python interpreter) Sets ssr to false in the layout.ts file to disable server-side rendering for the entire application. This enables client-side rendering for the entire application. Also refactors code copy function in Markdown component.
1 parent 7e2ab2a commit f8fc579

File tree

3 files changed

+40
-1
lines changed

3 files changed

+40
-1
lines changed

tools/server/server.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4911,6 +4911,42 @@ int main(int argc, char ** argv) {
49114911
svr->Get (params.api_prefix + "/slots", handle_slots);
49124912
svr->Post(params.api_prefix + "/slots/:id_slot", handle_slots_action);
49134913

4914+
// SPA fallback route - serve index.html for any route that doesn't match API endpoints
4915+
// This enables client-side routing for dynamic routes like /chat/[id]
4916+
if (params.public_path.empty()) {
4917+
// Only add fallback when using embedded static files
4918+
svr->Get(".*", [](const httplib::Request & req, httplib::Response & res) {
4919+
// Skip API routes - they should have been handled above
4920+
if (req.path.find("/v1/") != std::string::npos ||
4921+
req.path.find("/health") != std::string::npos ||
4922+
req.path.find("/metrics") != std::string::npos ||
4923+
req.path.find("/props") != std::string::npos ||
4924+
req.path.find("/models") != std::string::npos ||
4925+
req.path.find("/api/tags") != std::string::npos ||
4926+
req.path.find("/completions") != std::string::npos ||
4927+
req.path.find("/chat/completions") != std::string::npos ||
4928+
req.path.find("/embeddings") != std::string::npos ||
4929+
req.path.find("/tokenize") != std::string::npos ||
4930+
req.path.find("/detokenize") != std::string::npos ||
4931+
req.path.find("/lora-adapters") != std::string::npos ||
4932+
req.path.find("/slots") != std::string::npos) {
4933+
return false; // Let other handlers process API routes
4934+
}
4935+
4936+
// Serve index.html for all other routes (SPA fallback)
4937+
if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) {
4938+
res.set_content("Error: gzip is not supported by this browser", "text/plain");
4939+
} else {
4940+
res.set_header("Content-Encoding", "gzip");
4941+
// COEP and COOP headers, required by pyodide (python interpreter)
4942+
res.set_header("Cross-Origin-Embedder-Policy", "require-corp");
4943+
res.set_header("Cross-Origin-Opener-Policy", "same-origin");
4944+
res.set_content(reinterpret_cast<const char*>(index_html_gz), index_html_gz_len, "text/html; charset=utf-8");
4945+
}
4946+
return false;
4947+
});
4948+
}
4949+
49144950
//
49154951
// Start the server
49164952
//

tools/server/webui/src/lib/components/app/MarkdownContent.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import remarkRehype from 'remark-rehype';
88
import rehypeKatex from 'rehype-katex';
99
import rehypeStringify from 'rehype-stringify';
10+
import { copyCodeToClipboard } from '$lib/utils/copy';
1011
import 'highlight.js/styles/github-dark.css';
1112
import 'katex/dist/katex.min.css';
1213
@@ -143,7 +144,6 @@
143144
}
144145
145146
try {
146-
const { copyCodeToClipboard } = await import('$lib/utils/copy');
147147
await copyCodeToClipboard(rawCode);
148148
} catch (error) {
149149
console.error('Failed to copy code:', error);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const ssr = false;
2+
export const csr = true;
3+
export const prerender = false;

0 commit comments

Comments
 (0)