Server-Side Rendering (SSR) Implementation
PianoRhythm uses a sophisticated SSR setup built on SolidJS Start and Vinxi, providing fast initial page loads, SEO optimization, and seamless client-side hydration for a real-time musical collaboration platform.
Architecture Overview
Core Components
1. Vinxi Configuration (app.config.ts
)
The main build configuration that orchestrates the entire SSR setup:
export default defineConfig({
server: {
preset: "./preset",
minify: false,
sourceMap: true,
routeRules: {
"/": {
prerender: isProduction
},
"/*": {
cors: true,
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Resource-Policy": "cross-origin",
"x-pianorhythm-client-version": APP_VERSION ?? "0.0.0"
}
}
},
prerender: {
crawlLinks: isProduction
}
},
middleware: "./src/server/middleware",
vite: {
// Vite configuration for SSR
ssr: {
noExternal: [
"@hope-ui/solid",
"solid-dismiss"
]
},
// ... additional config
}
});
Key Features:
- Environment-specific builds: Different configurations for development, staging, and production
- Prerendering: Static generation for production builds
- CORS headers: Required for WebAssembly and audio worklets
- Custom presets: Tailored build configurations
2. Custom Preset (preset/nitro.config.ts
)
Nitro server configuration for different deployment targets:
import { defineNitroConfig } from "nitropack/config";
export default defineNitroConfig({
compatibilityDate: "2025-02-05",
// GitHub Pages deployment
preset: process.env.NODE_ENV === "production" ? "github-pages" : "node-server",
// Static site generation
prerender: {
routes: ["/", "/login"],
crawlLinks: true
},
// Server middleware
serverHandlers: [
{
route: "/api/**",
handler: "~/server/api/index.ts"
}
],
// Build optimizations
minify: process.env.NODE_ENV === "production",
sourceMap: process.env.NODE_ENV !== "production",
// Runtime configuration
runtimeConfig: {
public: {
clientVersion: process.env.PR_CLIENT_VERSION,
assetsUrl: process.env.PR_ASSETS_URL
}
}
});
3. Server Entry Point (src/entry-server.tsx
)
The server-side rendering entry point:
import { createHandler, StartServer } from "@solidjs/start/server";
import dns from 'node:dns';
import { COMMON } from "./util/const.common";
import { Database } from "./lib/db/db-store";
// Optimize DNS resolution for server environments
dns.setDefaultResultOrder('ipv4first');
console.log("🚀 Starting server with env:", process.env.BUILD_ENV ?? process.env.NODE_ENV);
console.log("✔ API Server:", process.env.PIANORHYTHM_SERVER_URL);
console.log("✔ Assets Server:", process.env.PR_ASSETS_URL);
console.log("✔ Client Version:", process.env.PR_CLIENT_VERSION);
// Initialize database connection
Database.getInstance().init();
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body>
<div id="app">{children}</div>
{scripts}
</body>
</html>
)}
/>
));
4. Client Entry Point (src/entry-client.tsx
)
The client-side hydration entry point:
import { mount, StartClient } from "@solidjs/start/client";
// Mount the application for client-side hydration
mount(() => <StartClient />, document.getElementById("app")!);
Rendering Pipeline
1. Server-Side Rendering Process
2. Route-Level Data Loading
// Route with server-side data loading
export const route = {
async preload(props) {
// Server-side data loading
if (props.intent == "initial") {
await onSessionRestore();
} else {
await getMemberSessionInfo();
}
}
} satisfies RouteDefinition;
export default function AppLoading(): JSX.Element {
// Component receives preloaded data
const navigate = useNavigate();
const [searchParams] = useSearchParams();
return <LoadingInterface />;
}
3. Progressive Enhancement
// Components that work with and without JavaScript
const InteractiveButton = () => {
const [clicked, setClicked] = createSignal(false);
return (
<button
onClick={() => setClicked(true)}
class={clicked() ? "clicked" : ""}
>
{clicked() ? "Clicked!" : "Click me"}
</button>
);
};
Static Site Generation
1. Prerendering Configuration
// Route rules for static generation
const routeRules = {
"/": { prerender: true },
"/login": { prerender: true },
"/room/*": { prerender: false }, // Dynamic routes
"/api/*": { prerender: false } // API routes
};
2. Build-Time Data Fetching
// Static data fetching during build
export const getStaticData = async () => {
const rooms = await fetchPublicRooms();
const stats = await fetchSiteStatistics();
return {
rooms,
stats,
generatedAt: new Date().toISOString()
};
};
3. Asset Optimization
// Vite configuration for asset optimization
const viteConfig = {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['solid-js', '@solidjs/router'],
audio: ['@core/pkg'],
ui: ['@hope-ui/solid']
}
}
}
}
};
Server Middleware
1. Custom Middleware (src/server/middleware.ts
)
import { createMiddleware } from "@solidjs/start/middleware";
import { setResponseHeader } from "vinxi/http";
export default createMiddleware({
onRequest: [
_event => {
// Request processing
}
],
onBeforeResponse: [
_event => {
// Set security headers required for WebAssembly
setResponseHeader("Cross-Origin-Embedder-Policy", "require-corp");
setResponseHeader("Cross-Origin-Opener-Policy", "same-origin");
setResponseHeader("Cross-Origin-Resource-Policy", "cross-origin");
}
]
});
2. API Route Handling
// API route example
export const POST = (event: APIEvent) =>
POST_API<InputDto, OutputDto>(event, {
schema: InputSchema,
process: async (input) => {
const dbService = SheetMusicDBService.getInstance();
const result = await dbService.aggregateData([
{ $match: { uuid: input.sheetMusicID } }
]);
return result;
}
});
3. Session Management
// Server-side session handling
export async function getSession() {
"use server";
return getRequestEvent()!.locals.session || {
data: {
accessToken: undefined,
refreshToken: undefined
}
};
}
export async function getMemberSessionInfo() {
"use server";
const session = await getSession();
try {
UserSessionHelper.validateTokens(session.data);
return session.data;
} catch {
// Handle invalid session
return null;
}
}
Environment Configuration
1. Environment Variables
// Server environment configuration
const envVariables = z.object({
DEBUG: z.string().default("false"),
NODE_ENV: z.string().default("dev"),
PR_CLIENT_VERSION: z.string().default("0.0.0"),
PR_ASSETS_URL: z.string().default("https://assets.pianorhythm.io"),
PIANORHYTHM_MONGODB_URI: z.string().default("mongodb://localhost:27017"),
PIANORHYTHM_SERVER_URL: z.string().default("http://localhost:7000"),
// ... additional environment variables
});
2. Runtime Configuration
// Runtime configuration based on environment
const getRuntimeConfig = () => {
const env = process.env.NODE_ENV;
return {
development: {
apiUrl: "http://localhost:7000",
assetsUrl: "http://localhost:3000",
debug: true
},
staging: {
apiUrl: "https://staging-api.pianorhythm.io",
assetsUrl: "https://staging-assets.pianorhythm.io",
debug: true
},
production: {
apiUrl: "https://api.pianorhythm.io",
assetsUrl: "https://assets.pianorhythm.io",
debug: false
}
}[env] || {};
};
Performance Optimizations
1. Code Splitting
// Automatic code splitting with lazy loading
const PianoRenderer = lazy(() => import('~/components/piano-renderer'));
const AudioVisualizer = lazy(() => import('~/components/audio-visualizer'));
const RoomComponent = () => {
return (
<Suspense fallback={<LoadingSpinner />}>
<PianoRenderer />
<AudioVisualizer />
</Suspense>
);
};
2. Resource Preloading
// Preload critical resources
const preloadCriticalResources = () => {
// Preload WASM modules
const wasmLink = document.createElement('link');
wasmLink.rel = 'preload';
wasmLink.href = '/pianorhythm_core/pkg/pianorhythm_core.wasm';
wasmLink.as = 'fetch';
wasmLink.crossOrigin = 'anonymous';
document.head.appendChild(wasmLink);
// Preload default soundfont
const soundfontLink = document.createElement('link');
soundfontLink.rel = 'preload';
soundfontLink.href = '/soundfonts/default.sf2';
soundfontLink.as = 'fetch';
document.head.appendChild(soundfontLink);
};
3. Caching Strategy
// Service worker for aggressive caching
const CACHE_NAME = 'pianorhythm-v1';
const STATIC_ASSETS = [
'/',
'/login',
'/pianorhythm_core/pkg/pianorhythm_core.wasm',
'/soundfonts/default.sf2'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
);
});
Deployment Strategies
1. GitHub Pages Deployment
# GitHub Actions workflow
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '19'
- name: Install dependencies
run: pnpm install
- name: Build for production
run: pnpm run build:production
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
2. Container Deployment
# Multi-stage Docker build
FROM node:19-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:19-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
RUN npm run build:production
EXPOSE 3000
CMD ["npm", "start"]
3. Edge Deployment
// Edge runtime configuration
export const config = {
runtime: 'edge',
regions: ['iad1', 'sfo1', 'fra1'] // Multiple regions for low latency
};
export default async function handler(request: Request) {
// Edge-optimized request handling
return new Response(await renderPage(request), {
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'public, max-age=3600'
}
});
}
Debugging & Monitoring
1. Server-Side Debugging
// Development server with debugging
if (process.env.NODE_ENV === 'development') {
console.log('🔧 Development mode enabled');
console.log('📊 Memory usage:', process.memoryUsage());
console.log('🌐 Environment variables:', {
NODE_ENV: process.env.NODE_ENV,
PORT: process.env.PORT,
API_URL: process.env.PIANORHYTHM_SERVER_URL
});
}
2. Performance Monitoring
// Server-side performance monitoring
const monitorSSRPerformance = (req: Request) => {
const startTime = Date.now();
return {
end: () => {
const duration = Date.now() - startTime;
console.log(`SSR rendered in ${duration}ms for ${req.url}`);
if (duration > 1000) {
console.warn(`Slow SSR render: ${duration}ms for ${req.url}`);
}
}
};
};
3. Error Tracking
// Server-side error handling
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Send to error tracking service
});
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Graceful shutdown
process.exit(1);
});
Next Steps
- Backend Services - API endpoints and database integration
- Deployment Guide - Production deployment strategies
- Build System - Detailed build configuration