• English
  • Server rendering

    What renders without a DOM, and how a character reaches the HTML before any JavaScript does.

    The two entry points

    ImportContainsSafe without a DOM
    @aora-bot/reactEvery component and hookNo
    @aora-bot/react/staticRendering functions and hashingYes

    The static entry deliberately excludes the animation runtime. Nothing it exports touches window, document or requestAnimationFrame, so it works in a React Server Component, during static generation, or in a worker.

    import { renderAvatarDataUrl } from '@aora-bot/react/static';
    
    export default async function Page() {
      return <img src={renderAvatarDataUrl('ada@example.com')} width={40} height={40} alt="" />;
    }

    Next.js App Router

    The component package ships with a 'use client' banner, so importing it from a server component is fine — the boundary is already declared.

    // app/page.tsx — a server component
    import { CharacterMark } from '@aora-bot/react';
    
    export default function Page() {
      return <CharacterMark shape="blob" color="blue" />;
    }

    Import the stylesheet once in the root layout:

    // app/layout.tsx
    import '@aora-bot/react/styles.css';

    Prefer the static entry where you can

    Characters in a long list, in an email, or in metadata do not need to animate. Using the static entry keeps the animation runtime out of the client bundle entirely.

    What server rendering produces

    Components render markup with no animation applied. The character appears in its rest pose, and the engine takes over on hydration. This means no layout shift: the server-rendered SVG occupies exactly the box the animated one will.

    <svg xmlns="http://www.w3.org/2000/svg" class="aga2w6z" viewBox="-24 -24 277 277" fill="none" aria-hidden="true"><style>
        .aga2w6z { --aga2w6z-ink: #28B06C; }
        @media (prefers-color-scheme: dark) {
          .aga2w6z { --aga2w6z-ink: #25A263; }
        }
      </style>
      <g transform="translate(114.5 114.5) scale(1) translate(-114.5 -114.5)">
        <path d="M114.5 9.678C116.086 9.678 117.671 9.682 119.257…
    import { renderStaticSvg } from '@aora-bot/react';
    
    export default function Demo() {
      const svg = renderStaticSvg({ shape: 'hex', color: 'green', expression: 'happy' });
      return (
        <div className="site-demo site-demo--column">
          <div dangerouslySetInnerHTML={{ __html: svg }} style={{ width: 90 }} />
          <pre style={{ maxHeight: 180, overflow: 'auto', fontSize: 12 }}>
            <code>{svg.slice(0, 400)}…</code>
          </pre>
        </div>
      );
    }

    Light and dark ink

    A static SVG is its own document as far as CSS goes, so it cannot read your application's theme class. renderStaticSvg therefore embeds the light and dark values of the palette ink, and colorScheme chooses what switches them:

    colorSchemeMechanismUse it for
    'system' (default)A prefers-color-scheme media query in the SVG<img src="data:…">, files, e-mail: the image is isolated from the page
    'page'light-dark(), resolved by the inherited color-schemeMarkup inlined into a page that has its own theme switch (dangerouslySetInnerHTML)

    system follows the operating system, which is what an isolated image can see. Inlined into a page whose theme the reader can switch, it would follow the OS instead of the page: a dark OS with a light site turns black ink near-white on a light card. page follows the same color-scheme the rest of your UI does. The default output is unchanged. The two modes use different class names, so both can appear in one document.

    renderStaticSvg({ shape: 'hex', color: 'black', colorScheme: 'page' });

    Data URLs

    renderStaticSvgDataUrl percent-encodes rather than base64-encodes. The output is smaller, and it stays readable in devtools. One detail it handles for you: a raw # in an SVG data URL terminates it at the fragment, which is the classic way inline SVG silently breaks.

    Next