blumenjs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ import React from "react";
2
+
3
+ // HMR: In development, load the client bundle from Webpack Dev Server
4
+ // so the HMR runtime + React Fast Refresh are active.
5
+ const isDev = process.env.NODE_ENV === "development";
6
+ const BUNDLE_SRC = isDev
7
+ ? "http://localhost:3100/static/js/bundle.js"
8
+ : "/static/js/bundle.js";
9
+
10
+ export function DefaultDocument({ children, initialProps }: any) {
11
+ const css = `
12
+ * { margin: 0; padding: 0; box-sizing: border-box; }
13
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.6; color: #e2e8f0; background: #1a1025; }
14
+ #root { min-height: 100vh; }
15
+ .about-page, .documentation-page { max-width: 800px; margin: 0 auto; padding: 2rem; color: #333; }
16
+ .about-page .page-content, .documentation-page .page-content { color: #333; }
17
+ .about-page .info-card, .documentation-page .info-card { background: white; color: #333; }
18
+ .about-page .page-header, .documentation-page .page-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
19
+ .home-page { min-height: 100vh; background: linear-gradient(180deg, #1a111d 0%, #2d1b4e 100%); padding: 0; display: flex; flex-direction: column; }
20
+ .hero { text-align: center; padding: 4rem 2rem 3rem; }
21
+ .logo { display: flex; align-items: center; justify-content: center; margin-bottom: 1.5rem; }
22
+ .logo h1 { font-size: 3rem; font-weight: 700; background: linear-gradient(135deg, #a855f7 0%, #c084fc 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
23
+ .version { margin-left: 0.75rem; padding: 0.25rem 0.5rem; background: rgba(168, 85, 247, 0.15); color: #c084fc; border-radius: 4px; font-size: 0.875rem; font-weight: 500; border: 1px solid rgba(168, 85, 247, 0.3); }
24
+ .tagline { color: #94a3b8; font-size: 1.25rem; line-height: 1.7; max-width: 600px; margin: 0 auto; }
25
+ .feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; max-width: 1000px; margin: 0 auto; padding: 0 2rem 3rem; }
26
+ .feature-card-large { grid-column: span 2; }
27
+ .feature-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 1.5rem; text-decoration: none; transition: all 0.2s ease; display: flex; flex-direction: column; }
28
+ .feature-card:hover { background: rgba(255, 255, 255, 0.06); border-color: rgba(168, 85, 247, 0.4); transform: translateY(-2px); }
29
+ .card-icon { display: flex; align-items: center; justify-content: center; width: 42px; height: 42px; background: rgba(168, 85, 247, 0.1); border: 1px solid rgba(168, 85, 247, 0.2); border-radius: 10px; color: #a855f7; margin-bottom: 1.25rem; transition: all 0.2s ease; }
30
+ .feature-card:hover .card-icon { background: rgba(168, 85, 247, 0.2); border-color: rgba(168, 85, 247, 0.4); transform: scale(1.05); }
31
+ .feature-card h3 { color: #f1f5f9; font-size: 1.125rem; margin-bottom: 0.5rem; font-weight: 600; }
32
+ .feature-card p { color: #94a3b8; font-size: 0.9rem; line-height: 1.6; margin-bottom: 1rem; flex-grow: 1; }
33
+ .card-code { background: rgba(0, 0, 0, 0.3); color: #c084fc; padding: 0.5rem 0.75rem; border-radius: 6px; font-family: "SF Mono", Monaco, monospace; font-size: 0.8rem; border: 1px solid rgba(168, 85, 247, 0.3); }
34
+ .home-footer { text-align: center; padding: 2rem; border-top: 1px solid rgba(168, 85, 247, 0.15); margin-top: auto; }
35
+ .footer-nav { display: flex; gap: 1.5rem; justify-content: center; margin-bottom: 1rem; }
36
+ .footer-link { color: #94a3b8; text-decoration: none; font-size: 0.9rem; transition: color 0.2s; }
37
+ .footer-link:hover { color: #c084fc; }
38
+ .footer-copy { color: #c084fc; font-size: 0.8rem; }
39
+ .page-header { text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 8px; }
40
+ .page-content { display: flex; flex-direction: column; gap: 1.5rem; }
41
+ .info-card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); }
42
+ .info-card h2 { color: #667eea; margin-bottom: 1rem; font-size: 1.3rem; }
43
+ .page-nav { margin-top: 2rem; text-align: center; }
44
+ .nav-link { display: inline-block; padding: 0.75rem 1.5rem; background: #667eea; color: white; text-decoration: none; border-radius: 4px; transition: background 0.2s; }
45
+ .nav-link:hover { background: #5a6fd6; }
46
+ .server-info { text-align: center; color: #666; margin-top: 1rem; }
47
+ .not-found { text-align: center; padding: 4rem 2rem; }
48
+ .not-found h1 { color: #667eea; font-size: 2rem; }
49
+ .page-transition { will-change: opacity; }
50
+ .page-transition-active { opacity: 1; transition: opacity 200ms ease-in; }
51
+ .page-transition-exit { opacity: 0; transition: opacity 150ms ease-out; }
52
+ `;
53
+
54
+ return (
55
+ <html lang="en">
56
+ <head>
57
+ <meta charSet="UTF-8" />
58
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
59
+ <meta name="description" content="SSR Go + React - Minimal Production-Quality SSR Engine" />
60
+ <title>SSR Go + React</title>
61
+ <style dangerouslySetInnerHTML={{ __html: css }} />
62
+ </head>
63
+ <body>
64
+ <div id="root">{children}</div>
65
+ <script
66
+ id="ssr-props"
67
+ type="application/json"
68
+ dangerouslySetInnerHTML={{
69
+ __html: JSON.stringify(initialProps).replace(/</g, '\\u003c')
70
+ }}
71
+ />
72
+ <script src={BUNDLE_SRC} defer></script>
73
+ </body>
74
+ </html>
75
+ );
76
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Blumen <Link> Component
3
+ *
4
+ * Drop-in replacement for <a> that enables SPA navigation.
5
+ * External links, new-tab links, and modified clicks (ctrl/cmd)
6
+ * are passed through to the browser as normal.
7
+ *
8
+ * During SSR (no RouterProvider), renders a plain <a> tag.
9
+ *
10
+ * @example
11
+ * <Link href="/about" className="nav-link">About</Link>
12
+ */
13
+
14
+ import React, { useContext } from "react";
15
+
16
+ // Import the context directly to do a safe check without throwing
17
+ // We need the raw context object, not the hook
18
+ import { RouterContextRef, useRouter } from "./RouterContext";
19
+
20
+ interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
21
+ /** The destination path */
22
+ href: string;
23
+ }
24
+
25
+ function isExternal(href: string): boolean {
26
+ return /^https?:\/\//.test(href) || href.startsWith("mailto:") || href.startsWith("tel:");
27
+ }
28
+
29
+ function isModifiedClick(e: React.MouseEvent): boolean {
30
+ return e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0;
31
+ }
32
+
33
+ export function Link({ href, children, onClick, target, ...rest }: LinkProps) {
34
+ // Safe check: if we're in SSR (no provider), just render a plain <a>
35
+ const ctx = useContext(RouterContextRef);
36
+
37
+ if (!ctx) {
38
+ // SSR fallback — no router available
39
+ return (
40
+ <a href={href} target={target} {...rest}>
41
+ {children}
42
+ </a>
43
+ );
44
+ }
45
+
46
+ const { navigate } = ctx;
47
+
48
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
49
+ // Let the caller's onClick run first
50
+ if (onClick) onClick(e);
51
+ if (e.defaultPrevented) return;
52
+
53
+ // Pass through to browser for:
54
+ // - External links
55
+ // - New-tab targets
56
+ // - Modified clicks (ctrl+click, cmd+click, etc.)
57
+ if (isExternal(href) || target === "_blank" || isModifiedClick(e)) {
58
+ return;
59
+ }
60
+
61
+ // SPA navigation
62
+ e.preventDefault();
63
+ navigate(href);
64
+ };
65
+
66
+ return (
67
+ <a href={href} onClick={handleClick} target={target} {...rest}>
68
+ {children}
69
+ </a>
70
+ );
71
+ }
72
+
73
+ export default Link;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Blumen Client-Side Router
3
+ *
4
+ * Provides SPA navigation without full page reloads.
5
+ * On the first load, SSR content is hydrated normally.
6
+ * Subsequent navigations swap React components client-side
7
+ * using the History API and the generated route map.
8
+ */
9
+
10
+ import React, {
11
+ createContext,
12
+ useContext,
13
+ useState,
14
+ useCallback,
15
+ useEffect,
16
+ useRef,
17
+ } from "react";
18
+ import { matchRoute, type RouteDef } from "./router";
19
+
20
+ // ── Context types ──────────────────────────────────────────────
21
+
22
+ interface RouterContextValue {
23
+ /** Current URL pathname */
24
+ path: string;
25
+ /** Extracted dynamic params for the current route (e.g. { id: "42" }) */
26
+ params: Record<string, string>;
27
+ /** Programmatic navigation */
28
+ navigate: (to: string) => void;
29
+ }
30
+
31
+ const RouterContext = createContext<RouterContextValue | null>(null);
32
+
33
+ // Exported for Link.tsx to do safe SSR checks without throwing
34
+ export { RouterContext as RouterContextRef };
35
+
36
+ /**
37
+ * Hook for page components to access the router.
38
+ *
39
+ * @example
40
+ * const { path, params, navigate } = useRouter();
41
+ */
42
+ export function useRouter(): RouterContextValue {
43
+ const ctx = useContext(RouterContext);
44
+ if (!ctx) {
45
+ throw new Error("useRouter must be used inside <RouterProvider>");
46
+ }
47
+ return ctx;
48
+ }
49
+
50
+ // ── Provider props ─────────────────────────────────────────────
51
+
52
+ interface RouterProviderProps {
53
+ /** The auto-generated route definitions */
54
+ routes: RouteDef[];
55
+ /** The App wrapper component */
56
+ App: React.ComponentType<any>;
57
+ /** The NotFound component for unmatched routes */
58
+ notFoundComponent: React.ComponentType<any>;
59
+ /** Initial SSR props passed from the server */
60
+ initialProps?: Record<string, any>;
61
+ children?: never; // Provider renders its own tree
62
+ }
63
+
64
+ // ── Provider ───────────────────────────────────────────────────
65
+
66
+ export function RouterProvider({
67
+ routes,
68
+ App,
69
+ notFoundComponent: NotFound,
70
+ initialProps = {},
71
+ }: RouterProviderProps) {
72
+ const [path, setPath] = useState<string>(
73
+ () => window.location.pathname,
74
+ );
75
+ const [transitioning, setTransitioning] = useState(false);
76
+ const [dynamicProps, setDynamicProps] = useState<Record<string, any>>({});
77
+
78
+ // Track whether we're on the very first render (SSR hydration).
79
+ // On the first render we re-use the server-provided props.
80
+ const isFirstRender = useRef(true);
81
+
82
+ // ── Route matching ──────────────────────────────────────────
83
+ const match = matchRoute(path, routes);
84
+ const PageComponent = match ? match.component : NotFound;
85
+ const params = match ? match.params : {};
86
+
87
+ // Build props for the page component
88
+ const pageProps = isFirstRender.current
89
+ ? { ...initialProps, params: { ...(initialProps.params || {}), ...params } }
90
+ : {
91
+ ...dynamicProps,
92
+ path,
93
+ params,
94
+ query: {},
95
+ timestamp: Date.now(),
96
+ serverRendered: false,
97
+ };
98
+
99
+ // After the first render we stop using the SSR props
100
+ useEffect(() => {
101
+ isFirstRender.current = false;
102
+ }, []);
103
+
104
+ // ── Navigation function ─────────────────────────────────────
105
+ const navigate = useCallback(
106
+ (to: string) => {
107
+ if (to === path) return; // no-op for same path
108
+
109
+ // Animate out → change route → animate in
110
+ setTransitioning(true);
111
+
112
+ // Fetch dynamic data from Go server
113
+ const fetchPromise = fetch(to, {
114
+ headers: { "X-Blumen-Data": "1" }
115
+ }).then(res => res.ok ? res.json() : {}).catch(err => {
116
+ console.error("Failed to fetch route data", err);
117
+ return {};
118
+ });
119
+
120
+ // Allow the exit transition to play (150ms)
121
+ const delayPromise = new Promise(resolve => setTimeout(resolve, 150));
122
+
123
+ Promise.all([fetchPromise, delayPromise]).then(([newData]) => {
124
+ setDynamicProps(newData);
125
+ window.history.pushState(null, "", to);
126
+ setPath(to);
127
+ window.scrollTo(0, 0);
128
+
129
+ // Small delay for the enter transition
130
+ requestAnimationFrame(() => {
131
+ setTransitioning(false);
132
+ });
133
+ });
134
+ },
135
+ [path],
136
+ );
137
+
138
+ // ── Back / Forward button support ───────────────────────────
139
+ useEffect(() => {
140
+ const onPopState = () => {
141
+ setTransitioning(true);
142
+ const to = window.location.pathname;
143
+
144
+ const fetchPromise = fetch(to, {
145
+ headers: { "X-Blumen-Data": "1" }
146
+ }).then(res => res.ok ? res.json() : {}).catch(() => ({}));
147
+
148
+ const delayPromise = new Promise(resolve => setTimeout(resolve, 150));
149
+
150
+ Promise.all([fetchPromise, delayPromise]).then(([newData]) => {
151
+ setDynamicProps(newData);
152
+ setPath(to);
153
+
154
+ requestAnimationFrame(() => {
155
+ setTransitioning(false);
156
+ });
157
+ });
158
+ };
159
+
160
+ window.addEventListener("popstate", onPopState);
161
+ return () => window.removeEventListener("popstate", onPopState);
162
+ }, []);
163
+
164
+ // ── Render ──────────────────────────────────────────────────
165
+ const contextValue: RouterContextValue = { path, params, navigate };
166
+
167
+ return (
168
+ <RouterContext.Provider value={contextValue}>
169
+ <div
170
+ className={`page-transition ${transitioning ? "page-transition-exit" : "page-transition-active"}`}
171
+ >
172
+ <App Component={PageComponent} pageProps={pageProps} />
173
+ </div>
174
+ </RouterContext.Provider>
175
+ );
176
+ }
@@ -0,0 +1,23 @@
1
+ export interface RouteDef {
2
+ path: string;
3
+ pattern: RegExp;
4
+ keys: string[];
5
+ component: React.ComponentType<any>;
6
+ }
7
+
8
+ export function matchRoute(path: string, routes: RouteDef[]) {
9
+ // Strip trailing slash for matching (unless it's exactly "/")
10
+ const normalizedPath = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
11
+
12
+ for (const route of routes) {
13
+ const match = normalizedPath.match(route.pattern);
14
+ if (match) {
15
+ const params: Record<string, string> = {};
16
+ route.keys.forEach((key: string, index: number) => {
17
+ params[key] = match[index + 1];
18
+ });
19
+ return { component: route.component, params };
20
+ }
21
+ }
22
+ return null;
23
+ }
@@ -0,0 +1,175 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "log"
8
+ "net"
9
+ "net/http"
10
+ "time"
11
+ )
12
+
13
+ const (
14
+ nodeSSRURL = "http://localhost:4000/render"
15
+ )
16
+
17
+ // SSRResponse from Node service
18
+ type SSRResponse struct {
19
+ HTML string `json:"html"`
20
+ Props map[string]interface{} `json:"props"`
21
+ }
22
+
23
+ // SSRRequest to Node service
24
+ type SSRRequest struct {
25
+ Path string `json:"path"`
26
+ Query map[string][]string `json:"query"`
27
+ Params map[string]interface{} `json:"params"`
28
+ Data map[string]interface{} `json:"data,omitempty"`
29
+ }
30
+
31
+ var httpClient = &http.Client{
32
+ Timeout: 10 * time.Second,
33
+ }
34
+
35
+ func main() {
36
+ mux := http.NewServeMux()
37
+
38
+ // Static files are served directly — must be registered first
39
+ mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
40
+
41
+ // Specific routes with Go loaders for data fetching
42
+ mux.HandleFunc("/dashboard/settings", PageHandler(func(r *http.Request) (map[string]interface{}, error) {
43
+ return map[string]interface{}{
44
+ "serverMessage": "Welcome to the settings. This data was securely fetched from Go at " + time.Now().Format(time.RFC1123),
45
+ "stats": map[string]int{"users": 150, "sales": 4200},
46
+ }, nil
47
+ }))
48
+
49
+ mux.HandleFunc("/users/{id}", PageHandler(func(r *http.Request) (map[string]interface{}, error) {
50
+ id := r.PathValue("id")
51
+ return map[string]interface{}{
52
+ "userId": id,
53
+ "userProfile": "Profile data for user " + id + " fetched from Go db",
54
+ }, nil
55
+ }))
56
+
57
+ // Catch-all: forward every other request to the Node SSR server.
58
+ // This decouples Go from knowing about specific React page paths.
59
+ mux.HandleFunc("/", PageHandler(nil))
60
+
61
+ startPort := 3000
62
+ var listener net.Listener
63
+ var err error
64
+
65
+ for {
66
+ addr := fmt.Sprintf(":%d", startPort)
67
+ listener, err = net.Listen("tcp", addr)
68
+ if err == nil {
69
+ break
70
+ }
71
+ log.Printf("Port %d is in use, trying %d...", startPort, startPort+1)
72
+ startPort++
73
+ }
74
+
75
+ log.Printf("Go server starting on http://localhost:%d", startPort)
76
+ if err := http.Serve(listener, mux); err != nil {
77
+ log.Fatalf("Server error: %v", err)
78
+ }
79
+ }
80
+
81
+ type DataLoader func(r *http.Request) (map[string]interface{}, error)
82
+
83
+ func PageHandler(loader DataLoader) http.HandlerFunc {
84
+ return func(w http.ResponseWriter, r *http.Request) {
85
+ if r.Method != http.MethodGet {
86
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
87
+ return
88
+ }
89
+
90
+ var props map[string]interface{}
91
+ var err error
92
+ if loader != nil {
93
+ props, err = loader(r)
94
+ if err != nil {
95
+ log.Printf("Loader error: %v", err)
96
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
97
+ return
98
+ }
99
+ }
100
+
101
+ // If this is a SPA data fetch request, return JSON props
102
+ if r.Header.Get("X-Blumen-Data") == "1" || r.URL.Query().Get("_data") == "1" {
103
+ w.Header().Set("Content-Type", "application/json")
104
+ if props == nil {
105
+ props = make(map[string]interface{})
106
+ }
107
+ json.NewEncoder(w).Encode(props)
108
+ return
109
+ }
110
+
111
+ handleRouteWithProps(w, r, props)
112
+ }
113
+ }
114
+
115
+ func handleRouteWithProps(w http.ResponseWriter, r *http.Request, props map[string]interface{}) {
116
+ // Prepare SSR request
117
+ ssrReq := SSRRequest{
118
+ Path: r.URL.Path,
119
+ Query: r.URL.Query(),
120
+ Params: map[string]interface{}{
121
+ "url": r.URL.String(),
122
+ "headers": r.Header,
123
+ },
124
+ Data: props,
125
+ }
126
+
127
+ // Call Node SSR service
128
+ ssrResp, err := callNodeSSR(ssrReq)
129
+ if err != nil {
130
+ log.Printf("SSR error: %v", err)
131
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
132
+ return
133
+ }
134
+
135
+
136
+
137
+ // Security headers
138
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
139
+ w.Header().Set("X-Content-Type-Options", "nosniff")
140
+ w.Header().Set("X-Frame-Options", "DENY")
141
+ w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
142
+
143
+ w.WriteHeader(http.StatusOK)
144
+ w.Write([]byte(ssrResp.HTML))
145
+ }
146
+
147
+ func callNodeSSR(req SSRRequest) (*SSRResponse, error) {
148
+ reqBody, err := json.Marshal(req)
149
+ if err != nil {
150
+ return nil, fmt.Errorf("marshal request: %w", err)
151
+ }
152
+
153
+ resp, err := httpClient.Post(
154
+ nodeSSRURL,
155
+ "application/json",
156
+ bytes.NewReader(reqBody),
157
+ )
158
+ if err != nil {
159
+ return nil, fmt.Errorf("http post: %w", err)
160
+ }
161
+ defer resp.Body.Close()
162
+
163
+ if resp.StatusCode != http.StatusOK {
164
+ return nil, fmt.Errorf("node ssr returned %d", resp.StatusCode)
165
+ }
166
+
167
+ var ssrResp SSRResponse
168
+ if err := json.NewDecoder(resp.Body).Decode(&ssrResp); err != nil {
169
+ return nil, fmt.Errorf("decode response: %w", err)
170
+ }
171
+
172
+ return &ssrResp, nil
173
+ }
174
+
175
+
@@ -0,0 +1,141 @@
1
+ import * as http from "http";
2
+ import { renderToString } from "react-dom/server";
3
+ import React from "react";
4
+
5
+ // Auto-generated route map (run `npm run routes` to regenerate)
6
+ import { routes, App, Document } from "./generated-routes";
7
+ import { matchRoute } from "../app/shared/router";
8
+ import NotFoundPage from "../app/pages/NotFound";
9
+
10
+ const NotFound = NotFoundPage;
11
+
12
+ interface SSRRequest {
13
+ path: string;
14
+ query: Record<string, string[]>;
15
+ params: Record<string, any>;
16
+ data?: Record<string, any>;
17
+ }
18
+
19
+ interface SSRResponse {
20
+ html: string;
21
+ props: Record<string, any>;
22
+ }
23
+
24
+ const PORT = process.env.PORT || 4000;
25
+
26
+ const server = http.createServer(async (req, res) => {
27
+ // CORS and security headers
28
+ res.setHeader("Content-Type", "application/json");
29
+ res.setHeader("Access-Control-Allow-Origin", "*");
30
+ res.setHeader("Access-Control-Allow-Methods", "POST");
31
+ res.setHeader("X-Content-Type-Options", "nosniff");
32
+
33
+ if (req.method === "OPTIONS") {
34
+ res.writeHead(200);
35
+ res.end();
36
+ return;
37
+ }
38
+
39
+ if (req.method !== "POST" || req.url !== "/render") {
40
+ res.writeHead(404);
41
+ res.end(JSON.stringify({ error: "Not Found" }));
42
+ return;
43
+ }
44
+
45
+ try {
46
+ // Parse request body
47
+ const body = await readBody(req);
48
+ const ssrReq: SSRRequest = JSON.parse(body);
49
+
50
+ // Match route
51
+ const match = matchRoute(ssrReq.path, routes);
52
+
53
+ if (!match) {
54
+ // Render 404 page
55
+ const props = {
56
+ path: ssrReq.path,
57
+ status: 404,
58
+ serverRendered: true,
59
+ };
60
+ const element = React.createElement(NotFound, props);
61
+ const html = renderToString(element);
62
+
63
+ res.writeHead(200);
64
+ res.end(JSON.stringify({ html, props }));
65
+ return;
66
+ }
67
+
68
+ // Prepare props for the page
69
+ const props = {
70
+ ...(ssrReq.data || {}),
71
+ path: ssrReq.path,
72
+ query: ssrReq.query,
73
+ params: { ...ssrReq.params, ...match.params },
74
+ timestamp: Date.now(),
75
+ serverRendered: true,
76
+ };
77
+
78
+ // Render React to HTML
79
+ const appElement = React.createElement(App, {
80
+ Component: match.component,
81
+ pageProps: props,
82
+ });
83
+
84
+ const documentElement = React.createElement(Document, {
85
+ initialProps: props,
86
+ children: appElement,
87
+ });
88
+
89
+ const html = "<!doctype html>\n" + renderToString(documentElement);
90
+
91
+ const ssrResp: SSRResponse = {
92
+ html,
93
+ props,
94
+ };
95
+
96
+ res.writeHead(200);
97
+ res.end(JSON.stringify(ssrResp));
98
+ } catch (error) {
99
+ console.error("SSR Error:", error);
100
+ res.writeHead(500);
101
+ res.end(
102
+ JSON.stringify({
103
+ error: "Internal Server Error",
104
+ message:
105
+ process.env.NODE_ENV === "development" ? String(error) : undefined,
106
+ }),
107
+ );
108
+ }
109
+ });
110
+
111
+ function readBody(req: http.IncomingMessage): Promise<string> {
112
+ return new Promise((resolve, reject) => {
113
+ let body = "";
114
+ req.on("data", (chunk) => {
115
+ body += chunk.toString();
116
+ });
117
+ req.on("end", () => {
118
+ resolve(body);
119
+ });
120
+ req.on("error", reject);
121
+ });
122
+ }
123
+
124
+ server.listen(PORT, () => {
125
+ console.log(`Node SSR server running on http://localhost:${PORT}`);
126
+ });
127
+
128
+ // Handle graceful shutdown
129
+ process.on("SIGTERM", () => {
130
+ console.log("SIGTERM received, shutting down...");
131
+ server.close(() => {
132
+ process.exit(0);
133
+ });
134
+ });
135
+
136
+ process.on("SIGINT", () => {
137
+ console.log("SIGINT received, shutting down...");
138
+ server.close(() => {
139
+ process.exit(0);
140
+ });
141
+ });