Skip to content

Server-Side Rendering (SSR) ​

Blaze-ng can render templates to HTML strings on the server for fast initial page loads, SEO, and email generation.

Basic SSR ​

Every template and view can be rendered to an HTML string:

ts
import { Blaze } from '@blaze-ng/core';
import { Template } from '@blaze-ng/templating-runtime';

// Render a template to HTML
const html = Blaze.toHTML(Template.myTemplate);

// Render with data
const html = Blaze.toHTMLWithData(Template.userCard, {
  name: 'Alice',
  email: 'alice@example.com',
  avatar: '/avatars/alice.jpg',
});

Rendering Compiled Templates ​

ts
import { SpacebarsCompiler } from '@blaze-ng/spacebars-compiler';

// Compile a template
const renderFn = SpacebarsCompiler.compile('<h1>Hello, {{name}}!</h1>');

// Create a template
const tmpl = new Template('greeting', renderFn);

// Render to HTML
const html = Blaze.toHTMLWithData(tmpl, { name: 'World' });
// => '<h1>Hello, World!</h1>'

SSR Patterns ​

Page Shell ​

ts
function renderPage(templateName, data) {
  const content = Blaze.toHTMLWithData(Template[templateName], data);

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${data.title || 'My App'}</title>
  <meta name="description" content="${data.description || ''}">
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <div id="app">${content}</div>
  <script src="/client.js"></script>
</body>
</html>`;
}

Express Integration ​

ts
import express from 'express';
import { Blaze } from '@blaze-ng/core';
import { Template } from '@blaze-ng/templating-runtime';

const app = express();

app.get('/', (req, res) => {
  const html = Blaze.toHTMLWithData(Template.homePage, {
    title: 'Welcome',
    features: getFeatures(),
  });
  res.send(renderShell(html, { title: 'Home' }));
});

app.get('/users/:id', async (req, res) => {
  const user = await db.users.findOne(req.params.id);
  if (!user) {
    const html = Blaze.toHTML(Template.notFound);
    return res.status(404).send(renderShell(html, { title: 'Not Found' }));
  }

  const html = Blaze.toHTMLWithData(Template.userProfile, { user });
  res.send(
    renderShell(html, {
      title: `${user.name} — Profile`,
      description: user.bio,
    }),
  );
});

Meteor SSR ​

ts
import { WebApp } from 'meteor/webapp';
import { Blaze } from '@blaze-ng/core';
import { Template } from '@blaze-ng/templating-runtime';

WebApp.connectHandlers.use('/ssr', (req, res) => {
  const html = Blaze.toHTMLWithData(Template.ssrPage, {
    url: req.url,
    user: req.user,
  });

  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end(html);
});

Email Templates ​

SSR is perfect for generating HTML emails:

handlebars
<template name='welcomeEmail'>
  <div style='font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;'>
    <div style='background: #4f46e5; color: white; padding: 32px; text-align: center;'>
      <h1 style='margin: 0;'>Welcome to {{appName}}!</h1>
    </div>
    <div style='padding: 32px;'>
      <p>Hi {{user.name}},</p>
      <p>Thanks for signing up! Here's what you can do next:</p>
      <ul>
        {{#each step in steps}}
          <li style='margin-bottom: 8px;'>
            <strong>{{step.title}}</strong>
            —
            {{step.description}}
          </li>
        {{/each}}
      </ul>
      <div style='text-align: center; margin: 32px 0;'>
        <a
          href='{{dashboardUrl}}'
          style='background: #4f46e5; color: white; padding: 12px 24px; 
                  text-decoration: none; border-radius: 6px;'
        >
          Go to Dashboard
        </a>
      </div>
    </div>
    <div style='background: #f3f4f6; padding: 16px; text-align: center; font-size: 12px;'>
      <p>{{appName}} — {{tagline}}</p>
      <a href='{{unsubscribeUrl}}'>Unsubscribe</a>
    </div>
  </div>
</template>
ts
function sendWelcomeEmail(user) {
  const html = Blaze.toHTMLWithData(Template.welcomeEmail, {
    appName: 'MyApp',
    tagline: 'Build amazing things',
    user,
    dashboardUrl: `https://myapp.com/dashboard`,
    unsubscribeUrl: `https://myapp.com/unsubscribe?token=${user.unsubToken}`,
    steps: [
      { title: 'Complete your profile', description: 'Add a photo and bio' },
      { title: 'Create a project', description: 'Start building something cool' },
      { title: 'Invite your team', description: 'Collaborate in real-time' },
    ],
  });

  Email.send({
    to: user.email,
    from: 'hello@myapp.com',
    subject: 'Welcome to MyApp!',
    html,
  });
}

PDF Generation ​

Combine SSR with a PDF library:

ts
import puppeteer from 'puppeteer';

async function generateInvoicePdf(invoice) {
  const html = Blaze.toHTMLWithData(Template.invoice, {
    invoice,
    company: getCompanyInfo(),
    formatCurrency: (amount) => `$${amount.toFixed(2)}`,
  });

  const fullHtml = `
    <!DOCTYPE html>
    <html>
    <head><style>${getInvoiceStyles()}</style></head>
    <body>${html}</body>
    </html>
  `;

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(fullHtml);
  const pdf = await page.pdf({ format: 'A4' });
  await browser.close();

  return pdf;
}

Static Site Generation ​

Pre-render pages at build time:

ts
import { writeFileSync, mkdirSync } from 'fs';
import { Blaze } from '@blaze-ng/core';
import { Template } from '@blaze-ng/templating-runtime';

const pages = [
  { path: '/', template: 'homePage', data: { title: 'Home' } },
  { path: '/about', template: 'aboutPage', data: { title: 'About' } },
  { path: '/pricing', template: 'pricingPage', data: { title: 'Pricing', plans: getPlans() } },
];

// Generate static HTML files
for (const page of pages) {
  const content = Blaze.toHTMLWithData(Template[page.template], page.data);
  const html = renderShell(content, page.data);

  const dir = `dist${page.path}`;
  mkdirSync(dir, { recursive: true });
  writeFileSync(`${dir}/index.html`, html);
}

console.log(`Generated ${pages.length} static pages`);

Component Snapshots for Testing ​

Use SSR to test component output:

ts
import { describe, it, expect } from 'vitest';
import { Blaze } from '@blaze-ng/core';
import { Template } from '@blaze-ng/templating-runtime';

describe('UserCard', () => {
  it('renders user information', () => {
    const html = Blaze.toHTMLWithData(Template.userCard, {
      name: 'Alice',
      email: 'alice@example.com',
      role: 'Admin',
    });

    expect(html).toContain('Alice');
    expect(html).toContain('alice@example.com');
    expect(html).toContain('Admin');
  });

  it('shows placeholder for missing avatar', () => {
    const html = Blaze.toHTMLWithData(Template.userCard, {
      name: 'Bob',
      avatar: null,
    });

    expect(html).toContain('avatar-placeholder');
    expect(html).not.toContain('<img');
  });

  it('matches snapshot', () => {
    const html = Blaze.toHTMLWithData(Template.userCard, {
      name: 'Test User',
      email: 'test@example.com',
    });

    expect(html).toMatchSnapshot();
  });
});

Performance Tips ​

  1. Cache compiled templates — compile once, render many times
  2. Minimize helpers — complex helper logic runs on every render
  3. Use toHTML over render — no DOM needed on the server
  4. Stream large pages — for very large pages, consider streaming chunks
  5. Pre-render static parts — cache parts that don't change per-request

Released under the MIT License.