Produce deterministic SVG on the server and let the browser adopt it as a live diagram without rebuilding or re-laying out the scene.
When to use this
Use server rendering when the first response must already contain the diagram: a page, email, README image, or cached SVG. The server path uses the same engine and SVG renderer as the browser, but it does not require a DOM.
renderStatic() is the small API from @grafloria/element. It returns a StaticRenderResult containing html, svg, css, and snapshot. Use svg when the result is an image or file. Use html, css, and snapshot when the browser must adopt the markup as an interactive diagram.
Render the server response
Pass positioned nodes and edges to renderToStaticSVG(). Give the render a fixed size and an instanceId when a page contains more than one server-rendered diagram. fitView: true frames the content using fitPadding.
tsimport { renderToStaticSVG, type StaticRenderOptions } from '@grafloria/renderer';
const options: StaticRenderOptions = {
nodes: [
{ id: 'extract', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } },
{ id: 'load', label: 'Load', position: { x: 260, y: 40 }, size: { width: 150, height: 66 } },
{ id: 'model', label: 'Model', position: { x: 150, y: 170 }, size: { width: 150, height: 66 } },
],
edges: [
{ id: 'extract-load', source: 'extract', target: 'load' },
{ id: 'extract-model', source: 'extract', target: 'model' },
],
width: 520,
height: 300,
instanceId: 'pipeline-diagram',
standalone: true,
};
const result = renderToStaticSVG(options);
export const pageDiagram = {
html: result.html,
css: result.css,
snapshot: result.snapshot,
};
export const imageSvg = result.svg;
The response contains three labelled boxes and two connecting edges. result.svg is a complete standalone SVG because standalone adds the SVG namespace. result.html is the layer skeleton intended for a diagram container; send result.css in a <style> element as well.
The output is deterministic for the same options. Omitted node and edge IDs receive deterministic node-<i> and edge-<i> IDs, auto-ports receive stable names, and the snapshot carries the instance scope, canvas size, camera origin, and zoom.
For the element package, the equivalent call is renderStatic(options). It is a re-export of the same server implementation:
tsimport { renderStatic, type StaticRenderOptions } from '@grafloria/element';
const options: StaticRenderOptions = {
nodes: [{ id: 'a', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } }],
edges: [],
width: 520,
height: 300,
standalone: true,
};
const { svg } = renderStatic(options);
Adopt the markup in the browser
On the client, pass the server snapshot to createDiagram(). Its DiagramInstance uses the existing server DOM, so the visible SVG remains in place while interaction becomes available.
tsimport { renderStatic } from '@grafloria/element';
import { createDiagram } from '@grafloria/renderer';
const serverDiagram = renderStatic({
nodes: [
{ id: 'extract', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } },
{ id: 'load', label: 'Load', position: { x: 260, y: 40 }, size: { width: 150, height: 66 } },
],
edges: [{ id: 'extract-load', source: 'extract', target: 'load' }],
width: 520,
height: 300,
});
const container = document.getElementById('pipeline') ?? document.body.appendChild(document.createElement('div'));
container.id = 'pipeline';
container.style.height = '300px';
container.style.width = '520px';
container.innerHTML = `<style>${serverDiagram.css}</style>${serverDiagram.html || serverDiagram.svg}`;
const instance = createDiagram(container, {
hydrate: serverDiagram.snapshot,
});
instance.fitView();
In an application, serialize the server object into the page's data transport rather than using the declare above. Do not call createDiagram() on the server: it requires a browser DOM. Do not replace the server HTML before hydration; that removes the DOM available for adoption.
Framework integrations
The server call is framework-independent. The client binding differs only in how it receives the returned html and snapshot.
tsximport { GrafloriaFlow } from '@grafloria/react';
import { renderStatic } from '@grafloria/element';
export function Pipeline() {
const ssr = renderStatic({ nodes: [{ id: 'extract', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } }], edges: [], width: 520, height: 300, standalone: true });
return <div style={{ height: 300 }}><GrafloriaFlow ssr={ssr} /></div>;
}
tsimport { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import { renderStatic } from '@grafloria/element';
import { CanvasNgCanvasNgComponent } from '@grafloria/canvas-ng';
import { createDiagram } from '@grafloria/renderer';
@Component({
standalone: true,
imports: [CanvasNgCanvasNgComponent],
template: '<lib-canvas-ng-canvas-ng></lib-canvas-ng-canvas-ng><div #host style="height: 300px"></div>',
})
export class PipelineComponent implements AfterViewInit {
@ViewChild('host', { static: true }) host!: ElementRef<HTMLElement>;
ngAfterViewInit(): void {
const result = renderStatic({ nodes: [{ id: 'extract', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } }], edges: [], width: 520, height: 300, standalone: true });
this.host.nativeElement.innerHTML = `<style>${result.css}</style>${result.html || result.svg}<div>${result.svg}</div>`;
createDiagram(this.host.nativeElement, { hydrate: result.snapshot });
}
}
tsximport { renderToStaticSVG } from '@grafloria/renderer';
import { GrafloriaFlow } from '@grafloria/qwik';
export function renderPipeline() {
return renderToStaticSVG({
nodes: [{ id: 'extract', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } }],
edges: [],
width: 520,
height: 300,
});
}
export { GrafloriaFlow };
tsimport { renderStatic } from '@grafloria/element';
import { GrafloriaFlow } from '@grafloria/vue';
export function renderPipeline() {
return renderStatic({
nodes: [{ id: 'extract', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } }],
edges: [],
width: 520,
height: 300,
});
}
export { GrafloriaFlow };
jsimport { renderStatic } from '@grafloria/element';
import { createDiagram } from '@grafloria/renderer';
const container = document.getElementById('pipeline') ?? document.body.appendChild(document.createElement('div'));
container.style.height = '300px';
container.style.width = '520px';
const serverDiagram = renderStatic({
nodes: [{ id: 'extract', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } }],
edges: [],
width: 520,
height: 300,
});
container.innerHTML = `<style>${serverDiagram.css}</style>${serverDiagram.html || serverDiagram.svg}<div>${serverDiagram.svg}</div>`;
createDiagram(container, { hydrate: serverDiagram.snapshot });
Each sample renders the same server result into a sized container. Use a framework binding when it owns the diagram component; use the two-call form when the host owns the container.
Custom or HTML-layer nodes are not rendered on the server because they are framework components. They mount on the client inside the correctly transformed HTML layer. SVG-rendered nodes, ports, edges, labels, arrows, and routing are included in the server result and snapshot.
Options that matter
| Option | Type | Default | What it does |
|---|---|---|---|
width | number | 800 | Sets the canvas width in CSS pixels. |
height | number | 600 | Sets the canvas height in CSS pixels. |
zoom | number | 1 | Sets the initial camera zoom. |
viewport | { x: number; y: number } | { x: 0, y: 0 } | Sets the camera origin in world coordinates. |
instanceId | string | 'grafloria-ssr' | Sets the diagram's CSS scope. Give each diagram on a page a different value. |
fitView | boolean | false | Frames the content instead of using viewport and zoom. |
fitPadding | number | 40 | Sets the padding used by fitView, in CSS pixels. |
standalone | boolean | false | Adds xmlns to the SVG so it stands alone as a file. |
See it running
Open the server-side export demo. Its preview is a pasted SVG, not a mounted canvas: the fingerprint stays the same across independent renders of the same specification, while a different specification changes it.
See the React starter for a live framework binding.
Related
- How Grafloria works — the shared model and engine.
- Instance and lifecycle — instance setup and teardown.
- Export diagrams — browser-side SVG, PNG, JPEG, WebP, and PDF export.
- Edit Mermaid diagrams — export and import text representations.
Was this page helpful?