Skip to content

Embed a Changelog

  • 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.

Drop this into any HTML page:

<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.

Pick how it shows with data-mode:

data-modeWhat it is
inline (default)The list, where the tag (or a <nordva-changelog> element) is. What a tag without data-mode has always shown
popupA small panel above a “What’s new” button, or under your own menu item
sidebarA panel that slides in from the right (from the left with data-position="bottom-left"), with search and label filters
modalThe same panel, centred
pageThe 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.

<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.

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:

<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: 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.

AttributeDefaultWhat it does
data-keyrequiredYour publishable key (nv_pub_…). A secret key is refused
data-modeinlineinline, popup, sidebar, modal or page
data-triggerCSS selector of your own element that opens the widget and carries the count
data-trigger-countnone: no badge inside your element (the data-nordva-unseen attribute is still set)
data-launchershownnone: no floating button (open it with NordvaChangelog.open())
data-positionbottom-rightWhere the button sits: bottom-right, bottom-left, top-right, top-left
data-user-id, data-user-hashThe signed-in user, for read state across devices and signed-in entries
data-themeautolight, dark or auto (follows the visitor’s system)
data-accent#0f766eAny CSS colour: buttons, pills, the count
data-widthPanel width in pixels, 280 to 640
data-limit5 inline, 10 otherwiseEntries per load, up to 50
data-labelsLabel slugs, comma-separated: show only entries with these labels, and only these filter chips
data-searchon in sidebar, modal and pagetrue or false: the search box and filter chips
data-reactionson, except inlinetrue or false
data-feedbackwhen the feedback widget is on the pagetrue or false: the “Give feedback on this” link. See below
data-feedback-urlYour own feedback page; opened with ?changelog_entry_id= when the feedback widget is not on the page
data-announcementstruefalse: never show the announced entry
data-show-badgefalseInline only: a “New” badge above the list until it is clicked (as in 1.x)
data-count-viewsCount landings per source, see 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-labelEnglishYour 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.

  • 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.

Each entry can carry a “Give feedback on this” link. It opens the feedback widget (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:

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 }).

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
});

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 lists them for you to copy.

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)

Section titled “Option B: Fetch in Astro (server-side, no auth)”
src/pages/changelog.astro
---
import { marked } from 'marked';
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)

Section titled “Option C: Fetch in React (client-side, no auth)”
import { useEffect, useState } from 'react';
import { marked } from 'marked';
interface ChangelogEntry {
id: string;
title: string;
body_markdown: string;
category: 'feature' | 'fix' | 'improvement' | 'security' | 'breaking';
version: string | null;
published_at: string;
public_url: string;
}
export function Changelog() {
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>
);
}

From the Nordva Launch dashboard → Changelog, or via the API:

Terminal window
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
}'

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

.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.

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>:

<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.

Add an email subscription form using a publishable key:

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: '[email protected]' }),
});
// 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.

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.

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.