Skip to main content

Publish a changelog on a static HTML page

Publish release notes from code, a CLI, or an AI coding agent. Entries are Markdown, can be scheduled, and are served on a hosted page at launch.nordva.dev/log/<your-slug> or on your own domain.

Keys

Publishing uses a secret key on your server. The public feed needs no key. Subscribing from a browser uses a publishable key. Create both under Dashboard → API keys.

Publishable key pasted into the script; it is designed to be public
NORDVA_SECRET_KEY=nv_live_…  # on your server only

01Server: Node.js

changelog-publish.ts

export async function publishEntry(input: Record<string, unknown>) {
  const res = await fetch("https://api.nordva.dev/v1/changelog/entries", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.NORDVA_SECRET_KEY!}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      title: input.title,
      body_markdown: input.body_markdown,
      category: input.category, // feature | fix | improvement | security | breaking
      version: input.version,
      notify_subscribers: true,
    }),
  });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  const { data } = await res.json();
  // data.status is "published", or "scheduled" if you passed scheduled_at
  // data.public_url is the hosted page for this entry
  return data;
}

Add an Idempotency-Key header when publishing from CI so a retried job cannot create a duplicate entry.

02Browser: render the public feed (no key)

// The public feed needs no key and allows any origin. body_markdown is
// Markdown; render it with your Markdown library of choice.
const res = await fetch("https://api.nordva.dev/public/changelog/your-project-slug");
const { data: entries } = await res.json();
for (const e of entries) {
  console.log(e.published_at, e.category, e.version, e.title, e.public_url);
}

03Browser: subscribe by email (publishable key)

const res = await fetch("https://api.nordva.dev/v1/changelog/subscribers", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email }),
});
const { data, error } = await res.json();
// 201 → data.confirmed is false until the reader clicks the confirmation link.
// Existing subscriber → error.code "ALREADY_SUBSCRIBED" (409).

Register the page's origin under Project → Allowed origins before calling this from a browser.

04Zero-code alternative: hosted widget

any HTML page

<script src="https://cdn.nordva.dev/v1/changelog.js"
  data-key="nv_pub_live_…"
  data-theme="auto"
  data-limit="5"
  data-show-badge="true"></script>
<nordva-changelog></nordva-changelog>

05Verify from a terminal

curl -s https://api.nordva.dev/public/changelog/your-project-slug | jq '.data[0]'

Behaviour worth knowing

  • Every response is { data, error, meta }. On failure error.code is a stable string such as VALIDATION_ERROR, PLAN_LIMIT_REACHED or RATE_LIMITED, with a remediation message.
  • POST and PATCH requests accept an Idempotency-Key header; the same key with the same body returns the original response for 24 hours.
  • Rate limits per key: 30 requests a minute on Free, 120 on Indie, 500 on Builder. A 429 carries Retry-After.
  • Browser calls with a publishable key must come from an origin registered on the project, otherwise the API answers ORIGIN_NOT_ALLOWED.