Skip to content

feat: enable server-side Blaze template rendering (foundation for SSG/SSR) - #507

Open
dupontbertrand wants to merge 15 commits into
meteor:release-3.1.0from
dupontbertrand:feature/static-render
Open

feat: enable server-side Blaze template rendering (foundation for SSG/SSR)#507
dupontbertrand wants to merge 15 commits into
meteor:release-3.1.0from
dupontbertrand:feature/static-render

Conversation

@dupontbertrand

Copy link
Copy Markdown

Summary

Enables Blaze.toHTML() to work on the server by making compiled templates and the Template registry available server-side. This is the foundation for SSG/SSR use cases (demonstrated in a working POC — see links below).

The key insight is that Blaze.toHTML() already works server-side through a DOM-free code path — the only problem was that compiled templates and the Template global were restricted to the client. This PR lifts that restriction without breaking anything client-side.

Changes

5 small, backward-compatible changes across 5 packages:

Package Change
templating-compiler Remove archMatching: 'web' so templates compile for all architectures
templating-tools Guard generateBodyJS() with Meteor.isClient — body rendering uses document.body
caching-html-compiler Guard body attrs setter with Meteor.isClient
templating-runtime Export Template to server, guard DOM code with Meteor.isClient
templating Export Template to server (matches templating-runtime)

Why this is safe

  • <template> tag compiled output is pure JavaScript (Template.__checkName + Template[name] = new Template(...)). No DOM access.
  • <body> tag compiled output uses document.body — wrapped in Meteor.isClient to stay client-only.
  • Template.__checkName, Template._migrateTemplate, Blaze.Template constructor are already server-safe (no DOM dependencies).
  • DOM-related code (Template.body, HMR, _applyHmrChanges) wrapped in Meteor.isClient to remain client-only.
  • Server-side overhead: ~500 bytes per compiled template. For an app with 100 templates, ~50 KB of JS loaded at startup.

What this does NOT change

  • Client-side rendering behavior is unchanged
  • blaze, htmljs, spacebars cores are unchanged
  • The DOM-dependent code (materializer, domrange, dombackend, events, attrs) stays client-only

Proof of concept

Built on top of these changes:

Forum discussion: https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556

Testing done

  • Existing Blaze usage (client-side rendering) continues to work unchanged
  • Template.myTemplate is now accessible server-side after import '../lib/templates.html' in server/main.js
  • Blaze.toHTML(Template.myTemplate) returns proper HTML strings server-side
  • Blaze.toHTMLWithData(Template.myTemplate, data) works with data contexts
  • #each, #if, #unless render correctly through _expandView(forExpansion=true)
  • Production meteor build succeeds
  • Vanilla Blaze apps (no SSG/SSR usage) build and run identically

Consumer-side usage

Apps that want to use server-side rendering can now do:

// server/main.js
import '../lib/templates.html';  // makes Template.* available server-side

// later — pure server-side rendering
const html = Blaze.toHTMLWithData(Template.myTemplate, { title: 'Hello' });

Templates must be written as pure render functions against explicit data — no Session, no this.subscribe(), no Template.dynamic (which remains client-only).

dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Apr 20, 2026
static-render is a server-side orchestration package that pre-renders
Blaze routes as HTML for SEO. Two modes:

- SSG (Static Site Generation): routes with static: 'ssg' are rendered
  once at server startup and cached permanently. Good for pages that
  rarely change (about, contact, terms).

- SSR (Server-Side Rendering): routes with static: 'ssr' are rendered
  on-the-fly at each request with fresh data from MongoDB via async
  staticData(). Good for product pages, articles, profiles.

Pre-rendered HTML is injected into the Meteor boilerplate via
req.dynamicBody and req.dynamicHead, so the client-side app still
loads and takes over normally (server pre-render + client takeover,
not React-style hydration).

The package auto-discovers routes from flow-router-extra (weak dep).
It also provides graceful error handling: if a template crashes
during rendering, a placeholder comment is rendered and the page
falls back to client-side rendering.

Requires the Blaze 3.1.x+ server-rendering changes (meteor/blaze#507).

Includes:
- packages/static-render/package.js, static-render-server.js, README.md
- v3-docs/docs/packages/static-render.md (full API docs)
- Sidebar entry in v3-docs/.vitepress/config.mts

Forum discussion:
https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
@jankapunkt

Copy link
Copy Markdown
Collaborator

I would target this for release 3.2.0 to keep 3.1.0 manageable and finish it soon

@dupontbertrand

dupontbertrand commented Apr 20, 2026

Copy link
Copy Markdown
Author

As you want boss, to be honest it's a "small" changes but if you prefer to wait for 3.2 it's up to you, maybe you have to check with someone else because this PR have to be merged at the same time at this one meteor/meteor#14355

dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Apr 20, 2026
static-render is a server-side orchestration package that pre-renders
Blaze routes as HTML for SEO. Two modes:

- SSG (Static Site Generation): routes with static: 'ssg' are rendered
  once at server startup and cached permanently. Good for pages that
  rarely change (about, contact, terms).

- SSR (Server-Side Rendering): routes with static: 'ssr' are rendered
  on-the-fly at each request with fresh data from MongoDB via async
  staticData(). Good for product pages, articles, profiles.

Pre-rendered HTML is injected into the Meteor boilerplate via
req.dynamicBody and req.dynamicHead, so the client-side app still
loads and takes over normally (server pre-render + client takeover,
not React-style hydration).

The package auto-discovers routes from flow-router-extra (weak dep).
It also provides graceful error handling: if a template crashes
during rendering, a placeholder comment is rendered and the page
falls back to client-side rendering.

Requires the Blaze 3.1.x+ server-rendering changes (meteor/blaze#507).

Includes:
- packages/static-render/package.js, static-render-server.js, README.md
- v3-docs/docs/packages/static-render.md (full API docs)
- Sidebar entry in v3-docs/.vitepress/config.mts

Forum discussion:
https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Apr 20, 2026
static-render is a server-side orchestration package that pre-renders
Blaze routes as HTML for SEO. Two modes:

- SSG (Static Site Generation): routes with static: 'ssg' are rendered
  once at server startup and cached permanently. Good for pages that
  rarely change (about, contact, terms).

- SSR (Server-Side Rendering): routes with static: 'ssr' are rendered
  on-the-fly at each request with fresh data from MongoDB via async
  staticData(). Good for product pages, articles, profiles.

Pre-rendered HTML is injected into the Meteor boilerplate via
req.dynamicBody and req.dynamicHead, so the client-side app still
loads and takes over normally (server pre-render + client takeover,
not React-style hydration).

The package auto-discovers routes from flow-router-extra (weak dep).
It also provides graceful error handling: if a template crashes
during rendering, a placeholder comment is rendered and the page
falls back to client-side rendering.

Requires the Blaze 3.1.x+ server-rendering changes (meteor/blaze#507).

Includes:
- packages/static-render/package.js, static-render-server.js, README.md
- v3-docs/docs/packages/static-render.md (full API docs)
- Sidebar entry in v3-docs/.vitepress/config.mts

Forum discussion:
https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Apr 20, 2026
static-render is a server-side orchestration package that pre-renders
Blaze routes as HTML for SEO. Two modes:

- SSG (Static Site Generation): routes with static: 'ssg' are rendered
  once at server startup and cached permanently. Good for pages that
  rarely change (about, contact, terms).

- SSR (Server-Side Rendering): routes with static: 'ssr' are rendered
  on-the-fly at each request with fresh data from MongoDB via async
  staticData(). Good for product pages, articles, profiles.

Pre-rendered HTML is injected into the Meteor boilerplate via
req.dynamicBody and req.dynamicHead, so the client-side app still
loads and takes over normally (server pre-render + client takeover,
not React-style hydration).

The package auto-discovers routes from flow-router-extra (weak dep).
It also provides graceful error handling: if a template crashes
during rendering, a placeholder comment is rendered and the page
falls back to client-side rendering.

Requires the Blaze 3.1.x+ server-rendering changes (meteor/blaze#507).

Includes:
- packages/static-render/package.js, static-render-server.js, README.md
- v3-docs/docs/packages/static-render.md (full API docs)
- Sidebar entry in v3-docs/.vitepress/config.mts

Forum discussion:
https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
@jankapunkt jankapunkt added this to the 3.2 milestone Apr 27, 2026
@dupontbertrand

dupontbertrand commented Jul 29, 2026

Copy link
Copy Markdown
Author

Cross-reference so the discussion stays in one place: the sequencing question for this PR is being discussed in meteor/meteor#14355, where @nachocodoner asked for E2E coverage of the static-render package. That E2E can only assert graceful degradation until this lands and the packages/non-core/blaze submodule in meteor/meteor moves off 3.0.3 — so the 3.1.0 vs 3.2 milestone call decides it. @jankapunkt the question is waiting for you there.

I measured the rebase in the meantime, and it is small: release-3.1.0 is 2 commits ahead of the merge base, and the only real conflict is packages/templating-runtime/package.js, where the version bumps (blaze@3.0.03.1.0-alpha.0) sit adjacent to the ], 'client') line this PR removes. Resolution is "take both sides", about 6 lines. packages/templating/package.js auto-merges. I'll clear it regardless of the milestone call — it presumes nothing.

One thing to fix in the same pass: this PR adds site/source/guide/server-rendering.md without a matching entry in site/.vitepress/config.mts, so the page is unreachable from the guide sidebar.

…for all architectures

This allows compiled Blaze templates to be available on the server, which
is required for server-side rendering use cases (SSG/SSR).

Server-side overhead is minimal (~500 bytes per template). The <template>
tag compiled output is pure JavaScript with no DOM dependencies. <body>
tag compilation is handled separately in templating-tools.
The <body> tag compiled code calls Template.body.renderToDocument which
uses document.body — this crashes on the server. Wrap generateBodyJS()
output with 'if (Meteor.isClient)' to make body compiled code safe to
load on both architectures.

generateTemplateJS() is unchanged — the <template> tag compiled output
(Template.__checkName + Template[name] = new Template(...)) is already
server-safe.
The generated code calls document.body.setAttribute() which crashes on
the server. Wrap it in 'if (Meteor.isClient)' to make the compiled HTML
safe to load on both architectures.
…teor.isClient

Export Template globally (both client and server) so that compiled
templates can register themselves server-side. Previously Template was
client-only, which prevented any server-side rendering use case.

Template.__checkName is already server-safe (no DOM). Template.body,
Template.__pendingReplacement, Template._applyHmrChanges, and the HMR
branch of Template._migrateTemplate all require the DOM — wrapped with
'if (Meteor.isClient)'.

dynamic.html and dynamic.js remain client-only (they require DOM).
Matches the export change in templating-runtime. The templating package
re-exports Template from templating-runtime and was also restricting it
to the client — which would override the templating-runtime change.
…or.isClient wrap

The simpleBody helper in html-scanner-tests now expects the <body> rendering
code to be wrapped in 'if (Meteor.isClient)', matching the change in
generateBodyJS().
Document the new server-side Blaze rendering capability:
- Blaze.toHTML() / Blaze.toHTMLWithData() work on the server
- How to make templates available server-side (import in server/main.js)
- Template restrictions (no Session, no this.subscribe, no Template.dynamic)
- Link to static-render package for higher-level SSG/SSR API
- Manual integration with server-render package
- Known rspack limitation
- Add dedicated SSG subsection with about-page example
- Add dedicated SSR subsection with product-page example
- Add SSG vs SSR comparison table for decision-making
The guide page added by this PR was not reachable from the Guide sidebar.
The static-render docs live in meteor/meteor's v3-docs, not in the Blaze
site — site/source/packages/ does not exist, so the relative links resolved
to nothing. Point them at v3-docs.meteor.com instead.
The guide claimed 'Blaze 3.1.x' three times while this PR is milestoned 3.2.
Describe the capability instead and let the release notes carry the version.
meteor#14350 (enable .html imports on server config for Blaze apps) merged
on 2026-04-20, so describing the patch as still required is stale. Also drops
a link to a page that does not exist in the Blaze docs site.
static-render bails out of route discovery, path matching and regeneration
when Package['ostrio:flow-router-extra'] is absent, so without it the package
is a no-op. Point routerless setups at Blaze.toHTML() directly.
Roughly 50 of the guide's lines tutorialised static-render, a package that
lives in meteor/meteor and is not merged yet. Blaze's guide documents Blaze's
capability; the package documents its own API. Keeps the SSG/SSR distinction,
drops the duplicated route examples and comparison table.
@dupontbertrand
dupontbertrand force-pushed the feature/static-render branch from f437642 to dba6aac Compare July 29, 2026 17:06
…ring

The guide claimed that 'if you don't import .html files from your server entry
point, nothing changes'. That is false for two classes of change this PR makes
unconditionally: blaze and spacebars join the server bundle of every app using
templating, because the 'client' scoping on templating-runtime's api.use was the
only thing excluding them; and removing archMatching: 'web' compiles .html for
the server too, so an eager-loading app registers every template server-side at
startup.

Neither renders anything on its own, but both cost bundle size and startup work,
and readers deciding whether to upgrade need to know.
dupontbertrand added a commit to dupontbertrand/meteor that referenced this pull request Jul 29, 2026
render() read Template[name] outside its try/catch. Blaze only exports the
Template registry to the server from 3.1+ (meteor/blaze#507); on any released
Blaze the identifier does not exist, so that read threw a ReferenceError which
propagated through _discoverAndRender and the startup hook into boot.js's
process.exit(1) — the package crashed the app at boot instead of degrading.

Check typeof Template first and return the same placeholder-comment shape used
for a missing template.
@dupontbertrand

Copy link
Copy Markdown
Author

Pushed one more docs commit.

The guide claimed that "if you don't import .html files from your server entry point, nothing changes". That is false for two changes this PR makes unconditionally to every app using templating:

  • blaze and spacebars join the server bundle — the 'client' scoping on templating-runtime's api.use was the only thing excluding them, and it has to be lifted for the registry to exist server-side.
  • removing archMatching: 'web' compiles .html for the server too, so an app that eager-loads (no imports/ directory) registers every template on the server at startup.

Neither renders anything by itself, and rendering stays opt-in behind an explicit Blaze.toHTML() call — but both cost server bundle size and startup work, and someone deciding whether to upgrade needs that stated. The section now distinguishes explicit-imports apps from eager-loading ones.

Still missing here, and I would rather name it than leave it implicit: there are no tests for the server-availability feature itself. The only test change is a scanner expectation update. Nothing asserts that Template.foo exists on the server or that toHTMLWithData renders on the os arch. I can add those if you want them in this PR rather than a follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants