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.
