Skip to content

Commit 6696a68

Browse files
authored
Merge pull request #1 from observablehq/mythmon/241118/readme
2 parents 14493a3 + a2f97cc commit 6696a68

File tree

3 files changed

+60
-31
lines changed

3 files changed

+60
-31
lines changed

README.md

+53-22
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,67 @@
1-
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1+
# Framework Embedded Analtycs in Next.js
22

3-
## Getting Started
3+
This repository is an example of importing a JS module from a [Observable Cloud](https://observablehq.com/documentation/data-apps/) data app using signed URLs and embedding it in a [Next.js](https://nextjs.org) 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.
44

5-
First, run the development server:
5+
## Tour
66

7-
```bash
8-
npm run dev
9-
# or
10-
yarn dev
11-
# or
12-
pnpm dev
13-
# or
14-
bun dev
7+
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.
8+
9+
The entry point for embedding is in [`src/app/page.tsx`](https://github.com/observablehq/nextjs-observable-embed/blob/main/src/app/page.tsx), which renders on the host application server and uses a React component to render the embed.
10+
11+
```tsx
12+
<ObservableEmbed
13+
module="https://observablehq.observablehq.cloud/olympian-embeds/medals-chart.js"
14+
importName="MedalsChart"
15+
/>
1516
```
1617

17-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18+
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`](https://github.com/panva/jose) is used to generate the JWT for the signed URL:
19+
20+
```tsx
21+
const parsedUrl = new URL(url);
22+
const now = Date.now();
23+
const notBefore = now - (now % SIGNATURE_ALIGN_MS);
24+
const notAfter = notBefore + SIGNATURE_VALIDITY_MS;
25+
const token = await new SignJWT({"urn:observablehq:path": parsedUrl.pathname})
26+
.setProtectedHeader({alg: "EdDSA"})
27+
.setSubject("nextjs-example")
28+
.setNotBefore(notBefore / 1000)
29+
.setExpirationTime(notAfter / 1000)
30+
.sign(privateKey);
31+
```
1832

19-
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
33+
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:
2034

21-
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
35+
```tsx
36+
const ref = useRef<HTMLDivElement | null>(null);
2237

23-
## Learn More
38+
useEffect(() => {
39+
(async () => {
40+
const target = ref.current;
41+
if (!target) return;
42+
while (target.firstChild) target.removeChild(target.firstChild);
43+
const mod = await import(/* webpackIgnore: true */ module);
44+
const component = mod[importName];
45+
const element = component instanceof Function ? await component() : component;
46+
target.append(element);
47+
})();
48+
}, [importName, module]);
2449

25-
To learn more about Next.js, take a look at the following resources:
50+
return <div ref={ref} />;
51+
```
52+
53+
## Development
2654

27-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
55+
Install dependencies:
2956

30-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
57+
```sh
58+
yarn install
59+
```
3160

32-
## Deploy on Vercel
61+
Run the development server:
3362

34-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
63+
```sh
64+
yarn dev
65+
```
3566

36-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
67+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

src/components/observableEmbed/client.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ export const ObservableEmbedClient: React.FC<ObservableEmbedProps> = ({
1616
(async () => {
1717
const target = ref.current;
1818
if (!target) return;
19+
while (target.firstChild) target.removeChild(target.firstChild);
1920
const mod = await import(/* webpackIgnore: true */ module);
2021
const component = mod[importName];
2122
const element = component instanceof Function ? await component() : component;
22-
target.innerHTML = "";
2323
target.append(element);
2424
})();
2525
}, [importName, module]);

src/components/observableEmbed/server.tsx

+6-8
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,21 @@ export const ObservableEmbed: React.FC<ObservableEmbedProps> = async ({ module,
1919
return <ObservableEmbedClient module={signedUrl.href} importName={importName} />;
2020
}
2121

22-
async function signUrl(url: string | URL): Promise<URL> {
23-
if (typeof url === "string") {
24-
url = new URL(url);
25-
}
22+
async function signUrl(url: string): Promise<URL> {
23+
const parsedUrl = new URL(url);
2624
if (!privateKey) {
2725
console.warn("No private key available for signing");
28-
return url;
26+
return parsedUrl;
2927
}
3028
const now = Date.now();
3129
const notBefore = now - (now % SIGNATURE_ALIGN_MS);
3230
const notAfter = notBefore + SIGNATURE_VALIDITY_MS;
33-
const token = await new SignJWT({"urn:observablehq:path": url.pathname})
31+
const token = await new SignJWT({"urn:observablehq:path": parsedUrl.pathname})
3432
.setProtectedHeader({alg: "EdDSA"})
3533
.setSubject("nextjs-example")
3634
.setNotBefore(notBefore / 1000)
3735
.setExpirationTime(notAfter / 1000)
3836
.sign(privateKey);
39-
url.searchParams.set("token", token);
40-
return url;
37+
parsedUrl.searchParams.set("token", token);
38+
return parsedUrl;
4139
}

0 commit comments

Comments
 (0)