Skip to content

observablehq/embedded-analytics-nextjs-app-example

Repository files navigation

Framework Embedded Analtycs in Next.js

This repository is an example of importing a JS module from a Observable Cloud data app using signed URLs and embedding it in a Next.js site using Next.js's App router. The Observable Cloud data app provides charts of the number of medals won by countries in the 2024 Olympic Games, optionally broken down by continent.

Tour

The repository uses Observable Cloud's signed URLs feature, which enables secure embedding for private data apps. In this example the data app is public, for demonstration purposes.

The entry point for embedding is in src/app/page.tsx, which renders on the host application server and uses a React component to render the embed.

<ObservableEmbed
  module="https://observablehq.observablehq.cloud/olympian-embeds/medals-chart.js"
  importName="MedalsChart"
/>

The ObservableEmbed component is imported from src/components/observableEmbed/server.tsx, and is a server-only component. Because of that it has access to the private key used to make signed URLs. jose is used to generate the JWT for the signed URL:

const parsedUrl = new URL(url);
const now = Date.now();
const notBefore = now - (now % SIGNATURE_ALIGN_MS);
const notAfter = notBefore + SIGNATURE_VALIDITY_MS;
const token = await new SignJWT({"urn:observablehq:path": parsedUrl.pathname})
  .setProtectedHeader({alg: "EdDSA"})
  .setSubject("nextjs-example")
  .setNotBefore(notBefore / 1000)
  .setExpirationTime(notAfter / 1000)
  .sign(privateKey);

This token is passed to the component ObservableEmbedClient, imported from src/components/observableEmbed/client.tsx. This is a client-only component, responsible for rendering the embed in the browser. It uses the signed URL generated by the Next.js server to import the module from Observable Cloud, and renders the imported component into the DOM:

const ref = useRef<HTMLDivElement | null>(null);

useEffect(() => {
  (async () => {
    const target = ref.current;
    if (!target) return;
    while (target.firstChild) target.removeChild(target.firstChild);
    const mod = await import(/* webpackIgnore: true */ module);
    const component = mod[importName];
    const element = component instanceof Function ? await component() : component;
    target.append(element);
  })();
}, [importName, module]);

return <div ref={ref} />;

Development

Install dependencies:

yarn install

Run the development server:

yarn dev

Open http://localhost:3000 with your browser to see the result.