feat: enable server-side Blaze template rendering (foundation for SSG/SSR) - #507
feat: enable server-side Blaze template rendering (foundation for SSG/SSR)#507dupontbertrand wants to merge 15 commits into
Conversation
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
|
I would target this for release 3.2.0 to keep 3.1.0 manageable and finish it soon |
|
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 |
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
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
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
|
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 I measured the rebase in the meantime, and it is small: One thing to fix in the same pass: this PR adds |
…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.
f437642 to
dba6aac
Compare
…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.
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.
|
Pushed one more docs commit. The guide claimed that "if you don't import
Neither renders anything by itself, and rendering stays opt-in behind an explicit 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 |
Summary
Enables
Blaze.toHTML()to work on the server by making compiled templates and theTemplateregistry 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 theTemplateglobal were restricted to the client. This PR lifts that restriction without breaking anything client-side.Changes
5 small, backward-compatible changes across 5 packages:
templating-compilerarchMatching: 'web'so templates compile for all architecturestemplating-toolsgenerateBodyJS()withMeteor.isClient— body rendering usesdocument.bodycaching-html-compilerMeteor.isClienttemplating-runtimeTemplateto server, guard DOM code withMeteor.isClienttemplatingTemplateto server (matchestemplating-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 usesdocument.body— wrapped inMeteor.isClientto stay client-only.Template.__checkName,Template._migrateTemplate,Blaze.Templateconstructor are already server-safe (no DOM dependencies).Template.body, HMR,_applyHmrChanges) wrapped inMeteor.isClientto remain client-only.What this does NOT change
blaze,htmljs,spacebarscores are unchangedmaterializer,domrange,dombackend,events,attrs) stays client-onlyProof of concept
Built on top of these changes:
@meteorjs/rspack— separate PR)Forum discussion: https://forums.meteor.com/t/ssg-ssr-for-meteor-blaze-a-proof-of-concept/64556
Testing done
Template.myTemplateis now accessible server-side afterimport '../lib/templates.html'inserver/main.jsBlaze.toHTML(Template.myTemplate)returns proper HTML strings server-sideBlaze.toHTMLWithData(Template.myTemplate, data)works with data contexts#each,#if,#unlessrender correctly through_expandView(forExpansion=true)meteor buildsucceedsConsumer-side usage
Apps that want to use server-side rendering can now do:
Templates must be written as pure render functions against explicit data — no
Session, nothis.subscribe(), noTemplate.dynamic(which remains client-only).