Server-Side Rendering (SSR) Guide

Learn how to implement server-side rendering with Coherent.js for fast initial page loads and SEO-friendly applications.

Why Server-Side Rendering?

Server-side rendering provides several benefits:

  • Fast Initial Load: HTML is rendered on the server, reducing time-to-first-paint
  • SEO Friendly: Search engines can crawl fully-rendered HTML
  • Progressive Enhancement: Works even with JavaScript disabled
  • Better Performance: Reduced client-side computation
  • Social Media: Meta tags and OpenGraph work correctly

Basic SSR Setup

Simple Server-Side Rendering

import { renderToString } from 'coherent-js';
import http from 'http';

// Define your component
const HomePage = ({ title, user }) => ({
  html: {
    children: [
      { head: {
        children: [
          { title: { text: title } },
          { meta: { charset: 'utf-8' } },
          { meta: { name: 'viewport', content: 'width=device-width, initial-scale=1' } }
        ]
      }},
      { body: {
        children: [
          { h1: { text: `Welcome, ${user.name}!` } },
          { p: { text: 'This page was rendered on the server.' } }
        ]
      }}
    ]
  }
});

// HTTP server
const server = http.createServer((req, res) => {
  const component = HomePage({ 
    title: 'My SSR App',
    user: { name: 'John Doe' }
  });
  
  const html = renderToString(component);
  
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end(`<!DOCTYPE html>${html}`);
});

server.listen(3000, () => {
  console.log('SSR Server running on http://localhost:3000');
});

Using Coherent Factory

import { createCoherent } from 'coherent-js';
import http from 'http';

// Create Coherent instance with SSR optimizations
const coherent = createCoherent({
  enableCache: true,        // Cache rendered components
  enableMonitoring: true,   // Monitor performance
  minify: true             // Minify output HTML
});

const server = http.createServer((req, res) => {
  try {
    const component = HomePage({ title: 'My App', user: { name: 'User' } });
    const html = coherent.render(component);
    
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end(`<!DOCTYPE html>${html}`);
    
  } catch (error) {
    console.error('SSR Error:', error);
    res.writeHead(500, { 'Content-Type': 'text/plain' });
    res.end('Internal Server Error');
  }
});

Complete HTML Document Structure

Full Page Component

const DocumentLayout = ({ title, description, children, scripts = [], styles = [] }) => ({
  html: {
    lang: 'en',
    children: [
      { head: {
        children: [
          { meta: { charset: 'utf-8' } },
          { meta: { name: 'viewport', content: 'width=device-width, initial-scale=1' } },
          { title: { text: title } },
          { meta: { name: 'description', content: description } },
          
          // CSS files
          ...styles.map(href => ({
            link: { rel: 'stylesheet', href }
          })),
          
          // Inline critical CSS
          { style: {
            text: `
              body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
              .container { max-width: 1200px; margin: 0 auto; }
            `
          }}
        ]
      }},
      { body: {
        children: [
          { div: {
            className: 'container',
            children: Array.isArray(children) ? children : [children]
          }},
          
          // JavaScript files
          ...scripts.map(src => ({
            script: { src, defer: true }
          }))
        ]
      }}
    ]
  }
});

Dynamic Meta Tags

const BlogPost = ({ post, baseUrl }) => {
  const fullUrl = `${baseUrl}/posts/${post.slug}`;
  
  return DocumentLayout({
    title: `${post.title} | My Blog`,
    description: post.excerpt,
    children: [
      // OpenGraph meta tags
      { meta: { property: 'og:title', content: post.title } },
      { meta: { property: 'og:description', content: post.excerpt } },
      { meta: { property: 'og:image', content: post.featuredImage } },
      { meta: { property: 'og:url', content: fullUrl } },
      { meta: { property: 'og:type', content: 'article' } },
      
      // Twitter Card
      { meta: { name: 'twitter:card', content: 'summary_large_image' } },
      { meta: { name: 'twitter:title', content: post.title } },
      { meta: { name: 'twitter:description', content: post.excerpt } },
      { meta: { name: 'twitter:image', content: post.featuredImage } },
      
      // Article content
      { article: {
        children: [
          { h1: { text: post.title } },
          { time: { datetime: post.publishedAt, text: new Date(post.publishedAt).toLocaleDateString() } },
          { div: { className: 'content', html: post.content } }
        ]
      }}
    ],
    styles: ['/css/blog.css'],
    scripts: ['/js/blog.js']
  });
};

Data Fetching for SSR

Async Data Loading

import { createDatabaseManager, createQuery, executeQuery } from 'coherent-js';

const db = createDatabaseManager({
  type: 'postgresql',
  host: 'localhost',
  database: 'blog'
});

async function renderBlogPost(slug) {
  // Fetch post data
  const postQuery = createQuery({
    table: 'posts',
    select: ['*'],
    where: { slug, published: true }
  });
  
  const [post] = await executeQuery(postQuery, db);
  
  if (!post) {
    throw new Error('Post not found');
  }
  
  // Fetch related comments
  const commentsQuery = createQuery({
    table: 'comments',
    select: ['id', 'author', 'content', 'created_at'],
    where: { post_id: post.id, approved: true },
    orderBy: [{ column: 'created_at', direction: 'ASC' }]
  });
  
  const comments = await executeQuery(commentsQuery, db);
  
  // Render component with data
  const component = BlogPostWithComments({ post, comments });
  return renderToString(component);
}

// Usage in server
const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  
  if (url.pathname.startsWith('/posts/')) {
    try {
      const slug = url.pathname.split('/posts/')[1];
      const html = await renderBlogPost(slug);
      
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(`<!DOCTYPE html>${html}`);
    } catch (error) {
      res.writeHead(404, { 'Content-Type': 'text/html' });
      res.end('<!DOCTYPE html><html><body><h1>Post not found</h1></body></html>');
    }
  }
});

Caching SSR Results

import { createCoherent } from 'coherent-js';

const coherent = createCoherent({
  enableCache: true,
  cacheSize: 1000,
  cacheTTL: 300000 // 5 minutes
});

// Simple in-memory cache for rendered pages
const pageCache = new Map();

async function renderWithCache(cacheKey, renderFunction) {
  // Check cache first
  if (pageCache.has(cacheKey)) {
    const cached = pageCache.get(cacheKey);
    if (Date.now() - cached.timestamp < 300000) { // 5 minutes
      return cached.html;
    }
    pageCache.delete(cacheKey);
  }
  
  // Render and cache
  const html = await renderFunction();
  pageCache.set(cacheKey, {
    html,
    timestamp: Date.now()
  });
  
  return html;
}

// Usage
async function handleBlogPost(slug) {
  return renderWithCache(`post:${slug}`, async () => {
    const postData = await fetchPostData(slug);
    const component = BlogPost(postData);
    return coherent.render(component);
  });
}

Streaming SSR

Streaming Large Pages

import { createStreamingRenderer } from 'coherent-js';

const streamRenderer = createStreamingRenderer({
  enableChunking: true,
  chunkSize: 1024
});

// Component with large content
const LargePage = ({ products = [] }) => ({
  html: {
    children: [
      { head: {
        children: [
          { title: { text: 'Product Catalog' } }
        ]
      }},
      { body: {
        children: [
          { h1: { text: 'Our Products' } },
          { div: {
            className: 'products-grid',
            children: products.map(product => ({
              div: {
                className: 'product-card',
                children: [
                  { h3: { text: product.name } },
                  { p: { text: product.description } },
                  { span: { text: `${product.price}` } }
                ]
              }
            }))
          }}
        ]
      }}
    ]
  }
});

// Stream response
const server = http.createServer(async (req, res) => {
  if (req.url === '/products') {
    try {
      const products = await fetchAllProducts(); // Large dataset
      const component = LargePage({ products });
      
      res.writeHead(200, { 
        'Content-Type': 'text/html',
        'Transfer-Encoding': 'chunked'
      });
      
      res.write('<!DOCTYPE html>');
      
      for await (const chunk of streamRenderer.stream(component)) {
        res.write(chunk);
      }
      
      res.end();
    } catch (error) {
      console.error('Streaming error:', error);
      res.writeHead(500).end('Error');
    }
  }
});

Progressive Content Loading

const ProgressivePage = ({ initialData, loadingPlaceholders }) => ({
  html: {
    children: [
      { head: {
        children: [
          { title: { text: 'Dashboard' } },
          { script: {
            text: `
              // Client-side loading script
              async function loadSection(sectionId, url) {
                const element = document.getElementById(sectionId);
                try {
                  const response = await fetch(url);
                  const html = await response.text();
                  element.innerHTML = html;
                } catch (error) {
                  element.innerHTML = '<p>Error loading content</p>';
                }
              }
              
              // Load sections when page is ready
              document.addEventListener('DOMContentLoaded', () => {
                loadSection('analytics', '/api/sections/analytics');
                loadSection('recent-activity', '/api/sections/activity');
              });
            `
          }}
        ]
      }},
      { body: {
        children: [
          { h1: { text: 'Dashboard' } },
          
          // Immediately available content
          { section: {
            children: [
              { h2: { text: 'Overview' } },
              { p: { text: `Welcome back, ${initialData.user.name}!` } }
            ]
          }},
          
          // Placeholder for lazy-loaded content
          { section: {
            id: 'analytics',
            className: 'loading',
            children: [
              { div: { className: 'spinner' } },
              { p: { text: 'Loading analytics...' } }
            ]
          }},
          
          { section: {
            id: 'recent-activity',
            className: 'loading',
            children: [
              { div: { className: 'spinner' } },
              { p: { text: 'Loading recent activity...' } }
            ]
          }}
        ]
      }}
    ]
  }
});

Error Handling in SSR

Error Boundaries

const ErrorBoundary = ({ error, children }) => {
  if (error) {
    return {
      div: {
        className: 'error-boundary',
        children: [
          { h2: { text: 'Something went wrong' } },
          { p: { text: 'Please try refreshing the page.' } },
          process.env.NODE_ENV === 'development' ? {
            details: {
              children: [
                { summary: { text: 'Error Details' } },
                { pre: { text: error.stack } }
              ]
            }
          } : null
        ].filter(Boolean)
      }
    };
  }
  
  return children;
};

// Usage in server
const server = http.createServer(async (req, res) => {
  try {
    const data = await fetchPageData(req.url);
    const component = ErrorBoundary({
      children: PageComponent(data)
    });
    
    const html = coherent.render(component);
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end(`<!DOCTYPE html>${html}`);
    
  } catch (error) {
    console.error('SSR Error:', error);
    
    const errorComponent = ErrorBoundary({
      error,
      children: null
    });
    
    const html = coherent.render(errorComponent);
    res.writeHead(500, { 'Content-Type': 'text/html' });
    res.end(`<!DOCTYPE html>${html}`);
  }
});

Graceful Degradation

const RobustComponent = ({ data, fallback }) => {
  try {
    // Validate required data
    if (!data || !data.items || !Array.isArray(data.items)) {
      throw new Error('Invalid data structure');
    }
    
    return {
      div: {
        className: 'content',
        children: data.items.map(item => ({
          div: {
            className: 'item',
            children: [
              { h3: { text: item.title || 'Untitled' } },
              { p: { text: item.description || 'No description available' } }
            ]
          }
        }))
      }
    };
    
  } catch (error) {
    console.warn('Component rendering failed, using fallback:', error.message);
    
    return fallback || {
      div: {
        className: 'fallback-content',
        children: [
          { p: { text: 'Content temporarily unavailable' } }
        ]
      }
    };
  }
};

Performance Optimization

Component Precompilation

import { precompileComponent } from 'coherent-js';

// Precompile static components
const precompiledHeader = precompileComponent({
  header: {
    children: [
      { h1: { text: 'My Website' } },
      { nav: {
        children: [
          { a: { href: '/', text: 'Home' } },
          { a: { href: '/about', text: 'About' } },
          { a: { href: '/contact', text: 'Contact' } }
        ]
      }}
    ]
  }
});

// Use in pages
const HomePage = ({ content }) => ({
  html: {
    children: [
      { head: { children: [{ title: { text: 'Home' } }] }},
      { body: {
        children: [
          precompiledHeader, // Pre-rendered HTML
          { main: { children: content } }
        ]
      }}
    ]
  }
});

Memory Usage Optimization

import { createCoherent, performanceMonitor } from 'coherent-js';

const coherent = createCoherent({
  enableCache: true,
  cacheSize: 500,  // Limit cache size
  enableMonitoring: true
});

// Monitor memory usage
setInterval(() => {
  const stats = coherent.getPerformanceStats();
  const memUsage = process.memoryUsage();
  
  console.log('SSR Performance Stats:', {
    renderTime: stats.monitor.avgRenderTime,
    cacheHitRate: stats.cache.hitRate,
    memoryUsage: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`
  });
  
  // Clear cache if memory usage is high
  if (memUsage.heapUsed > 500 * 1024 * 1024) { // 500MB
    coherent.clearCache();
    console.log('Cache cleared due to high memory usage');
  }
}, 60000); // Every minute

SEO Optimization

Structured Data

const ProductPage = ({ product }) => ({
  html: {
    children: [
      { head: {
        children: [
          { title: { text: `${product.name} | My Store` } },
          { meta: { name: 'description', content: product.description } },
          
          // Structured data for SEO
          { script: {
            type: 'application/ld+json',
            text: JSON.stringify({
              '@context': 'https://schema.org',
              '@type': 'Product',
              name: product.name,
              description: product.description,
              image: product.images,
              offers: {
                '@type': 'Offer',
                price: product.price,
                priceCurrency: 'USD',
                availability: product.inStock ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock'
              }
            })
          }}
        ]
      }},
      { body: {
        children: [
          { h1: { text: product.name } },
          { img: { src: product.images[0], alt: product.name } },
          { p: { text: product.description } },
          { span: { text: `${product.price}` } }
        ]
      }}
    ]
  }
});

Sitemap Generation

import { createQuery, executeQuery, createDatabaseManager } from 'coherent-js';

const db = createDatabaseManager({ type: 'postgresql', database: 'mysite' });

async function generateSitemap() {
  // Get all pages
  const pagesQuery = createQuery({
    table: 'pages',
    select: ['slug', 'updated_at'],
    where: { published: true }
  });
  
  const pages = await executeQuery(pagesQuery, db);
  
  // Generate sitemap XML
  const sitemap = {
    urlset: {
      xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9',
      children: pages.map(page => ({
        url: {
          children: [
            { loc: { text: `https://mysite.com/${page.slug}` } },
            { lastmod: { text: page.updated_at.toISOString().split('T')[0] } },
            { changefreq: { text: 'weekly' } },
            { priority: { text: '0.8' } }
          ]
        }
      }))
    }
  };
  
  return renderToString(sitemap);
}

Next Steps