# Embed a Changelog

> Display your Nordva Launch changelog on your website, with a script-tag widget or custom fetch.

## Prerequisites

- Nordva Launch account with at least one published changelog entry
- Any frontend framework (or plain HTML)

The public feed (`GET /public/changelog/<your-project-slug>`) needs no API key and allows any origin, so your visitors' browsers can call it directly. Entries carry `body_markdown`; render it with the Markdown library you already use, with HTML escaped.

The public feed only ever holds what anyone may see: published entries with audience `public` that have not passed their `expires_at`. Pinned entries come first. Each entry also carries `labels`, `cover_image_url`, `cta_label`, `cta_url` and `pinned`; `?q=` searches and `?label=` filters. For entries only signed-in users see, reactions, read state and announcements, use the widget endpoints in [Changelog widget API](/guides/changelog-widget-api/).

## Option A: Script-tag widget (zero code)

Drop this into any HTML page:

```html
<script src="https://cdn.nordva.dev/v1/changelog.js"
  data-key="nv_pub_live_YOUR_KEY"
  data-mode="popup"></script>
```

That adds a floating "What's new" button with the number of entries this visitor has not seen. Clicking it opens your latest entries; each opens in full, with its cover image, its button and reactions. Everything renders inside a Shadow DOM, so its styles never clash with your page, and it works under a strict [Content Security Policy](/guides/content-security-policy/).

Pick how it shows with `data-mode`:

| `data-mode` | What it is |
|---|---|
| `inline` (default) | The list, where the tag (or a `<nordva-changelog>` element) is. What a tag without `data-mode` has always shown |
| `popup` | A small panel above a "What's new" button, or under your own menu item |
| `sidebar` | A panel that slides in from the right (from the left with `data-position="bottom-left"`), with search and label filters |
| `modal` | The same panel, centred |
| `page` | The full list with search, filters and "Show more", for a `/changelog` route in your app. Put `<nordva-changelog data-mode="page"></nordva-changelog>` where it goes |

On phones the popup, sidebar and modal open as a bottom sheet.

### Use your own "What's new" link

```html
<a href="#" id="whats-new">What's new</a>

<script src="https://cdn.nordva.dev/v1/changelog.js"
  data-key="nv_pub_live_YOUR_KEY"
  data-mode="popup"
  data-trigger="#whats-new"></script>
```

With `data-trigger`, clicking your element opens the widget and no floating button is drawn. The widget puts the unseen count inside your element (a small badge in its own shadow root) and sets `data-nordva-unseen="2"` on it, so you can style it yourself instead; `data-trigger-count="none"` leaves the badge out. If your framework re-renders the menu, the badge follows the new element.

### Read state across devices

Without a user id, "new" means new in this browser since it last opened the widget. Give the widget the signed-in user and read state is kept on our side, so an entry read on a laptop is not new on a phone:

```html
<script src="https://cdn.nordva.dev/v1/changelog.js"
  data-key="nv_pub_live_YOUR_KEY"
  data-mode="sidebar"
  data-user-id="user_123"
  data-user-hash="HMAC_SHA256_OF_USER_ID_MADE_ON_YOUR_SERVER"></script>
```

It is the same `user_hash` as the [notification bell](/guides/notifications-identity/): one signing secret does both. Signed-in entries (`audience: "signed_in"`) and segment entries only show with a valid hash; a wrong hash stops the widget and says why in the console.

### Attributes

| Attribute | Default | What it does |
|---|---|---|
| `data-key` | required | Your publishable key (`nv_pub_…`). A secret key is refused |
| `data-mode` | `inline` | `inline`, `popup`, `sidebar`, `modal` or `page` |
| `data-trigger` | | CSS selector of your own element that opens the widget and carries the count |
| `data-trigger-count` | | `none`: no badge inside your element (the `data-nordva-unseen` attribute is still set) |
| `data-launcher` | shown | `none`: no floating button (open it with `NordvaChangelog.open()`) |
| `data-position` | `bottom-right` | Where the button sits: `bottom-right`, `bottom-left`, `top-right`, `top-left` |
| `data-user-id`, `data-user-hash` | | The signed-in user, for read state across devices and signed-in entries |
| `data-theme` | `auto` | `light`, `dark` or `auto` (follows the visitor's system) |
| `data-accent` | `#0f766e` | Any CSS colour: buttons, pills, the count |
| `data-width` | | Panel width in pixels, 280 to 640 |
| `data-limit` | `5` inline, `10` otherwise | Entries per load, up to 50 |
| `data-labels` | | Label slugs, comma-separated: show only entries with these labels, and only these filter chips |
| `data-search` | on in sidebar, modal and page | `true` or `false`: the search box and filter chips |
| `data-reactions` | on, except inline | `true` or `false` |
| `data-feedback` | when the feedback widget is on the page | `true` or `false`: the "Give feedback on this" link. See below |
| `data-feedback-url` | | Your own feedback page; opened with `?changelog_entry_id=` when the feedback widget is not on the page |
| `data-announcements` | `true` | `false`: never show the announced entry |
| `data-show-badge` | `false` | Inline only: a "New" badge above the list until it is clicked (as in 1.x) |
| `data-count-views` | | Count landings per source, see [Attribution](/guides/attribution/) |
| `data-title`, `data-launcher-label`, `data-empty`, `data-search-placeholder`, `data-all-label`, `data-see-all-label`, `data-subscribe-label`, `data-feedback-label`, `data-later-label`, `data-new-label`, `data-more-label` | English | Your own words for every label, set as text |

Attributes can also go on a `<nordva-changelog>` element, which wins over the script tag: two inline lists with different limits on one page, for example.

### What the widget shows

- **Entries**: pinned first, then newest. Each has its type (New, Improved, Fixed, Security, Breaking), labels, date and version, a dot while it is new to this person, the cover image, the text, the entry's button ("Try it") and reactions.
- **Text**: a small Markdown subset turned into page elements, never into HTML: paragraphs, `#` headings, `-` and `1.` lists, `>` quotes (use them as callouts), fenced code blocks, rules, `**bold**`, `*italic*`, `` `code` ``, links and images. Links must be `https://`, `http://`, `mailto:`, a path on your site (`/settings`) or an `#anchor`; anything else keeps its text and loses the link. Links to other sites open in a new tab. Images must be `https://` links and load with `referrerpolicy="no-referrer"` and `loading="lazy"`. Raw HTML is shown as text.
- **Announcement**: the one entry you announce (Indie and up) shows once per person as a modal, a top banner or a toast, in every mode, even with an inline list. The banner sits over the top of your page until it is dismissed.
- **Footer**: "See all updates" (your hosted page), "Get updates by email" when your plan has subscriber emails (it subscribes to the label chosen in the filter, if any), and "Powered by Nordva Launch" below Builder.

### Give feedback on this

Each entry can carry a "Give feedback on this" link. It opens the [feedback widget](/guides/feedback-react/) (1.4.0 or later) on the same page with the entry attached, so the message lands in your Feedback inbox linked to that entry. The link shows by itself when the feedback widget is on the page. With your own feedback form instead, set `data-feedback="true"` and listen for the event:

```js
window.addEventListener("nordva:changelog-feedback", (e) => {
  e.preventDefault(); // tells the widget you handled it
  openMyFeedbackForm({ changelogEntryId: e.detail.changelog_entry_id, about: e.detail.title });
});
```

The feedback widget has the same way in: `window.NordvaFeedback.open({ changelog_entry_id, title })`.

### JavaScript

```js
NordvaChangelog.open();          // open the panel (popup, sidebar, modal)
NordvaChangelog.open("ce_01…");  // open it at one entry
NordvaChangelog.close();
NordvaChangelog.toggle();
NordvaChangelog.refresh();       // fetch again, e.g. after sign-in
NordvaChangelog.unseen;          // the current count, or null before the first answer

window.addEventListener("nordva:changelog", (e) => {
  console.log(e.detail.unseen_count); // whenever the count changes
});
```

### What it stores in the browser

Nothing for a signed-in user (read state is kept on our side). Otherwise, in local storage: `nordva_changelog_seen` (inline "New" badge, as in 1.x), `nordva_changelog_opened` (when this browser last opened the widget; not in inline mode), `nordva_changelog_announced` (announcements already shown) and, only after a reaction, `nordva_changelog_client` (a random id sent with reactions). No cookies. The [Cookie Policy](https://launch.nordva.dev/cookies/) lists them for you to copy.

### Upgrading from 1.x

A tag without `data-mode` renders the same inline list, reads the same `data-limit`, `data-theme`, `data-show-badge` and `data-count-views`, and stores nothing new. Add `data-mode` when you want the rest.

## Option B: Fetch in Astro (server-side, no auth)

```astro
---
// src/pages/changelog.astro

const res = await fetch('https://api.nordva.dev/public/changelog/your-project-slug');
const { data: entries } = await res.json();
---

<main>
  {entries.map(entry => (
    <article>
      <time>{new Date(entry.published_at).toLocaleDateString()}</time>
      <h2><a href={entry.public_url}>{entry.title}</a></h2>
      <small>{entry.category}{entry.version ? ` · ${entry.version}` : ''}</small>
      <div set:html={marked.parse(entry.body_markdown)} />
    </article>
  ))}
</main>
```

No API key needed. This renders at build time in Astro's static mode.

## Option C: Fetch in React (client-side, no auth)

```tsx

interface ChangelogEntry {
  id: string;
  title: string;
  body_markdown: string;
  category: 'feature' | 'fix' | 'improvement' | 'security' | 'breaking';
  version: string | null;
  published_at: string;
  public_url: string;
}

  const [entries, setEntries] = useState<ChangelogEntry[]>([]);

  useEffect(() => {
    fetch('https://api.nordva.dev/public/changelog/your-project-slug')
      .then(r => r.json())
      .then(({ data }) => setEntries(data.slice(0, 10)));
  }, []);

  return (
    <ul>
      {entries.map(entry => (
        <li key={entry.id}>
          <time>{new Date(entry.published_at).toLocaleDateString()}</time>
          <h3><a href={entry.public_url}>{entry.title}</a></h3>
          <div dangerouslySetInnerHTML={{ __html: marked.parse(entry.body_markdown) }} />
        </li>
      ))}
    </ul>
  );
}
```

## Publish an entry

From the [Nordva Launch dashboard](https://app.nordva.dev) → **Changelog**, or via the API:

```bash
curl -X POST https://api.nordva.dev/v1/changelog/entries \
  -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Dark mode is here",
    "body_markdown": "## Dark mode\n\nYou can now toggle dark mode from settings.",
    "category": "feature",
    "notify_subscribers": true,
    "notify_in_app": true
  }'
```

## Publish from GitHub releases

A GitHub Action turns every published release into a changelog entry: the release notes become the body, the tag becomes the version.

```yaml
# .github/workflows/changelog.yml
name: Changelog
on:
  release:
    types: [published]
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: nordvadev/changelog-action@v1
        with:
          api_key: ${{ secrets.NORDVA_SECRET_KEY }}
          # category: feature      # feature, improvement, fix, security, breaking
          # notify_subscribers: true
```

Add `NORDVA_SECRET_KEY` as a repository secret. Drafts publish nothing, pre-releases are skipped unless you say otherwise, and re-running the workflow never creates a second entry (the release id is the idempotency key). Inputs and outputs are in the action's README.

## RSS, Atom and JSON Feed

Every changelog has three feeds. No key, no setup:

```
https://api.nordva.dev/public/changelog/your-project-slug/feed.xml    RSS 2.0
https://api.nordva.dev/public/changelog/your-project-slug/feed.atom   Atom 1.0
https://api.nordva.dev/public/changelog/your-project-slug/feed.json   JSON Feed 1.1
```

The hosted page at `launch.nordva.dev/log/your-project-slug` advertises them, so a feed reader pointed at the page finds them by itself. If you render the changelog on your own site, add the same tag to your `<head>`:

```html
<link rel="alternate" type="application/rss+xml" title="Changelog"
      href="https://api.nordva.dev/public/changelog/your-project-slug/feed.xml" />
```

Entry bodies are rendered from markdown to a small, escaped HTML subset (paragraphs, lists, bold, italic, code, links, and https images). Raw HTML in an entry is shown as text, never run. The cover image leads the entry and its button follows it as a link. Images are written with `referrerpolicy="no-referrer"` and `loading="lazy"`. Signed-in, segment and expired entries are never in a feed. Feeds are cached for five minutes.

## Let users subscribe by email

Add an email subscription form using a publishable key:

```typescript
const res = await fetch('https://api.nordva.dev/v1/changelog/subscribers', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer nv_pub_live_YOUR_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ email: 'user@example.com' }),
});
// 201 with confirmed: false: the person gets one email with a Confirm button
```

From a browser, a subscription waits for the person to confirm by email (anyone can type anyone's address), and the answer is the same whether or not the address was already subscribed. Unconfirmed subscriptions are removed after 7 days. With a secret key from your server the subscription is confirmed at once (`confirmed: true`, and 200 for an address already subscribed).

Add `labels: ["mobile", "api"]` (label slugs) to let a subscriber follow only some topics: they are then emailed only entries carrying one of those labels. Left out, they get every public entry. See [Labels and label subscriptions](/guides/changelog-widget-api/#labels).

Confirmed subscribers receive an email when you publish a public entry with `notify_subscribers: true`, whether the entry publishes at once or at its `scheduled_at`. The email is plain text in your project's name: the entry's title, its body, a link to the entry, and a one-click unsubscribe link that removes the subscriber. Emails go out in batches every few minutes within the platform's daily email quota, so a large list can take a while. Subscriber emails are on Indie and up; on Free the flag is stored but nobody is subscribed. In the dashboard the publish dialog has the same checkbox; over MCP, pass `notify_subscribers` to `publish_changelog_entry` or `update_changelog_entry`.

`notify_in_app: true` also posts the entry to your users' in-app bell when it publishes, once: one broadcast with the title, a short excerpt and a link to the entry. It is on every plan and counts once against the monthly notification limit. Left out, the project's `changelog_to_bell_default` setting decides. See [In-app notifications](/guides/notifications/#changelog-entries-in-the-bell).

If your site sends a `Content-Security-Policy` header, allow `https://cdn.nordva.dev` in `script-src`, `https://api.nordva.dev` in `connect-src`, and your image host in `img-src`: [Content Security Policy](/guides/content-security-policy).

Source: https://docs.nordva.dev/guides/changelog-embed/
