Insights by OmkarUrja

Framework recipes

Framework recipes

Copy-paste setups for the frameworks customers ship on today. Each recipe covers: theme loading, a character image, and a scene backdrop. If you haven't set up a key yet, start with the quickstart first.


Plain HTML — zero tooling

The lowest-friction integration. Works on GitHub Pages, a static Nginx, or any host that serves .html.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Urja · plain HTML</title>

    <link rel="preload"
          href="https://urja.insightsbyomkar.com/api/v1/fonts/display/display-400-normal-latin.woff2"
          as="font" type="font/woff2" crossorigin="anonymous" />
    <link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/tokens?format=css&version=1.4.0" />
    <link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/fonts?format=css&families=display,sans" />
    <link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/motion?format=css&set=all" />

    <style>
      body {
        background: var(--ds-bg-canvas);
        color: var(--ds-text-strong);
        font-family: var(--ds-font-sans);
      }
      h1 { font-family: var(--ds-font-display); }
      .card {
        background: var(--ds-bg-elevated);
        border-radius: var(--ds-radius-lg);
        padding: var(--ds-space-6);
      }
    </style>
  </head>
  <body>
    <h1>Hello from Urja</h1>

    <!-- Public character preview — no key required -->
    <img
      src="https://urja.insightsbyomkar.com/api/v1/character/demo?variant=listening&size=256"
      width="256" height="256"
      alt="Lucky listening"
    />

    <!-- Pricing-card backdrop — public -->
    <div class="card" style="position: relative; overflow: hidden;">
      <img
        src="https://urja.insightsbyomkar.com/api/v1/scene?type=pricing-card&product=netra&tier=pro"
        style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0;"
        alt="" />
      <div style="position: relative; z-index: 1;">
        <h2>Netra · Pro</h2>
        <p>$19/mo</p>
      </div>
    </div>
  </body>
</html>

Next.js App Router

The Urja tokens/fonts/motion CSS goes in your root layout; the authenticated character endpoint gets a one-file proxy so the JWT never reaches the browser.

app/layout.tsx

import { VisualThemeProvider } from "urja-client";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <VisualThemeProvider tokensVersion="1.4.0">
          {children}
        </VisualThemeProvider>
      </body>
    </html>
  );
}

app/api/urja/character/route.ts (server proxy)

import { NextRequest, NextResponse } from "next/server";

export const runtime = "nodejs";

export async function GET(req: NextRequest) {
  const incoming = new URL(req.url);
  const upstream = new URL("https://urja.insightsbyomkar.com/api/v1/character");
  incoming.searchParams.forEach((v, k) => upstream.searchParams.set(k, v));

  const res = await fetch(upstream, {
    headers: { authorization: `Bearer ${process.env.URJA_API_KEY!}` },
  });

  return new NextResponse(res.body, {
    status: res.status,
    headers: {
      "content-type": res.headers.get("content-type") ?? "image/svg+xml",
      "cache-control":
        res.headers.get("cache-control") ??
        "public, max-age=86400, s-maxage=604800, immutable",
    },
  });
}

Consuming the proxy + typed URL builders

import { buildPricingCardUrl, buildUmbrellaHeroUrl } from "urja-client";

export default function Pricing() {
  return (
    <section>
      {/* Authenticated character — through the proxy */}
      <img
        src="/api/urja/character?pose=listening&mood=attentive&size=320"
        width={320}
        height={320}
        alt="Lucky listening"
      />

      {/* Public scene — straight from Urja */}
      <img
        src={buildPricingCardUrl({ product: "netra", tier: "max" })}
        width={600}
        height={400}
        alt=""
      />

      <img
        src={buildUmbrellaHeroUrl({ motion: "drift" })}
        width={1600}
        height={800}
        alt="Insights by Omkar ecosystem"
      />
    </section>
  );
}

Astro

Use the same <link> tags in your root layout — the Urja CSS is framework-agnostic.

src/layouts/Base.astro

---
// Astro layout — no scripting needed for theme loading
---
<html lang="en">
  <head>
    <link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/tokens?format=css&version=1.4.0" />
    <link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/fonts?format=css&families=display,sans" />
    <link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/motion?format=css&set=all" />
  </head>
  <body>
    <slot />
  </body>
</html>

Astro API route proxy — src/pages/api/urja/character.ts

import type { APIRoute } from "astro";

export const GET: APIRoute = async ({ url, request: _request }) => {
  const upstream = new URL("https://urja.insightsbyomkar.com/api/v1/character");
  url.searchParams.forEach((v, k) => upstream.searchParams.set(k, v));

  const res = await fetch(upstream, {
    headers: {
      authorization: `Bearer ${import.meta.env.URJA_API_KEY}`,
    },
  });

  return new Response(res.body, {
    status: res.status,
    headers: {
      "content-type": res.headers.get("content-type") ?? "image/svg+xml",
      "cache-control":
        res.headers.get("cache-control") ??
        "public, max-age=86400, s-maxage=604800, immutable",
    },
  });
};

SvelteKit

+layout.svelte carries the <link> tags; the proxy uses SvelteKit's +server.ts API route convention.

src/routes/+layout.svelte

<svelte:head>
  <link rel="stylesheet"
        href="https://urja.insightsbyomkar.com/api/v1/tokens?format=css&version=1.4.0" />
  <link rel="stylesheet"
        href="https://urja.insightsbyomkar.com/api/v1/fonts?format=css&families=display,sans" />
  <link rel="stylesheet"
        href="https://urja.insightsbyomkar.com/api/v1/motion?format=css&set=all" />
</svelte:head>

<slot />

src/routes/api/urja/character/+server.ts

import type { RequestHandler } from "./$types";
import { URJA_API_KEY } from "$env/static/private";

export const GET: RequestHandler = async ({ url, fetch }) => {
  const upstream = new URL("https://urja.insightsbyomkar.com/api/v1/character");
  url.searchParams.forEach((v, k) => upstream.searchParams.set(k, v));

  const res = await fetch(upstream, {
    headers: { authorization: `Bearer ${URJA_API_KEY}` },
  });

  return new Response(res.body, {
    status: res.status,
    headers: {
      "content-type": res.headers.get("content-type") ?? "image/svg+xml",
      "cache-control":
        res.headers.get("cache-control") ??
        "public, max-age=86400, s-maxage=604800, immutable",
    },
  });
};

Consuming from a Svelte component

<script lang="ts">
  import { buildPricingCardUrl, buildUmbrellaHeroUrl } from "urja-client";
</script>

<img
  src={buildPricingCardUrl({ product: "netra", tier: "max" })}
  width={600} height={400} alt="" />

<!-- Authenticated character — through the same-origin proxy -->
<img
  src="/api/urja/character?pose=listening&mood=attentive&size=320"
  width={320} height={320} alt="Lucky listening" />

Nuxt 3 / Nuxt 4

app.vue carries the <Head> tags; the proxy lives under server/api/.

app.vue

<template>
  <Head>
    <Link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/tokens?format=css&version=1.4.0" />
    <Link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/fonts?format=css&families=display,sans" />
    <Link rel="stylesheet"
          href="https://urja.insightsbyomkar.com/api/v1/motion?format=css&set=all" />
  </Head>
  <NuxtPage />
</template>

server/api/urja/character.get.ts

import { defineEventHandler, sendStream, getQuery } from "h3";

export default defineEventHandler(async (event) => {
  const upstream = new URL("https://urja.insightsbyomkar.com/api/v1/character");
  const params = getQuery(event);
  for (const [k, v] of Object.entries(params)) {
    if (v !== undefined && v !== null) upstream.searchParams.set(k, String(v));
  }

  const res = await fetch(upstream, {
    headers: { authorization: `Bearer ${process.env.URJA_API_KEY}` },
  });

  event.node.res.statusCode = res.status;
  event.node.res.setHeader(
    "content-type",
    res.headers.get("content-type") ?? "image/svg+xml",
  );
  event.node.res.setHeader(
    "cache-control",
    res.headers.get("cache-control") ??
      "public, max-age=86400, s-maxage=604800, immutable",
  );
  return sendStream(event, res.body!);
});

Consuming from a Vue component

<script setup lang="ts">
import { buildPricingCardUrl } from "urja-client";
const bg = buildPricingCardUrl({ product: "lucky", tier: "pro" });
</script>

<template>
  <img :src="bg" :width="600" :height="400" alt="" />
  <img
    src="/api/urja/character?pose=listening&size=320"
    :width="320" :height="320" alt="Lucky listening" />
</template>

Vanilla JS — single-file widget

If you want to drop Urja into a non-framework site, the <link> tags above are all you need for CSS. For runtime theme injection (e.g. an SPA that mounts into a third-party page), call injectVisualTheme():

import { injectVisualTheme } from "urja-client";

// Call once on widget mount. Returns a disposer if you need to tear
// it down later.
const dispose = injectVisualTheme({
  baseUrl: "https://urja.insightsbyomkar.com",
  tokensVersion: "1.4.0",
  fontFamilies: ["display", "sans"],
  motionSet: "all",
});

// later
// dispose();

All the URL builders work outside of React too:

import { buildPricingCardUrl } from "urja-client";

const img = document.createElement("img");
img.src = buildPricingCardUrl({ product: "lucky", tier: "pro" });
img.width = 600;
img.height = 400;
document.body.appendChild(img);

Handling rate limits

Every authenticated response returns X-RateLimit-Remaining-Minute + X-RateLimit-Remaining-Day. In your proxy, read them and decide:

const res = await fetch(upstream, {
  headers: { authorization: `Bearer ${process.env.URJA_API_KEY!}` },
});

if (res.status === 429) {
  const retry = Number(res.headers.get("retry-after") ?? "60");
  // Back off + maybe serve a placeholder SVG from the CDN
  return new Response(PLACEHOLDER_SVG, {
    status: 503,
    headers: {
      "content-type": "image/svg+xml",
      "retry-after": String(retry),
      "cache-control": "public, max-age=60",
    },
  });
}

const remainingMin = Number(res.headers.get("x-ratelimit-remaining-minute") ?? "0");
if (remainingMin < 10) {
  // Pace outbound traffic or warm cache aggressively
}

If you're staying well inside quota, you can skip this — Urja's cache headers do most of the work. The guidance above applies only if your traffic spikes unpredictably.


What else

  • Building a static site generator, CMS plugin, or framework-specific

adapter? The URL builders in urja-client are SSR/CSR-agnostic — they just return strings.

  • Want recipes for a framework not listed here? Email

admin@insightsbyomkar.com with your stack and we'll add it.