Embed a Changelog
Prerequisites
Section titled “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.
Option A: Script-tag widget (zero code)
Section titled “Option A: Script-tag widget (zero code)”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-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
Section titled “Use your own “What’s new” link”<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
Section titled “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:
<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.
Attributes
Section titled “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 | |
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
Section titled “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,-and1.lists,>quotes (use them as callouts), fenced code blocks, rules,**bold**,*italic*,`code`, links and images. Links must behttps://,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 behttps://links and load withreferrerpolicy="no-referrer"andloading="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
Section titled “Give feedback on this”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 }).
JavaScript
Section titled “JavaScript”NordvaChangelog.open(); // open the panel (popup, sidebar, modal)NordvaChangelog.open("ce_01…"); // open it at one entryNordvaChangelog.close();NordvaChangelog.toggle();NordvaChangelog.refresh(); // fetch again, e.g. after sign-inNordvaChangelog.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
Section titled “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 lists them for you to copy.
Upgrading from 1.x
Section titled “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)
Section titled “Option B: Fetch in Astro (server-side, no auth)”---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> );}Publish an entry
Section titled “Publish an entry”From the Nordva Launch dashboard → Changelog, or via the API:
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
Section titled “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.
name: Changelogon: 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: trueAdd 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
Section titled “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.0https://api.nordva.dev/public/changelog/your-project-slug/feed.atom Atom 1.0https://api.nordva.dev/public/changelog/your-project-slug/feed.json JSON Feed 1.1The 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.
Let users subscribe by email
Section titled “Let users subscribe by email”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', },});// 201 with confirmed: false: the person gets one email with a Confirm buttonFrom 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.