Publish a changelog in an Astro site
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.
PUBLIC_NORDVA_PUBLISHABLE_KEY=nv_pub_live_… # .env
NORDVA_SECRET_KEY=nv_live_… # .env, server only 01Server: API route
src/pages/api/changelog-publish.ts
import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request }) => {
const input = await request.json();
const res = await fetch("https://api.nordva.dev/v1/changelog/entries", {
method: "POST",
headers: {
Authorization: `Bearer ${import.meta.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 new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } });
}; 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)
src/pages/changelog.astro
---
// Runs at build or request time on the server. No key needed for the public feed.
const res = await fetch("https://api.nordva.dev/public/changelog/your-project-slug");
const { data: entries } = await res.json();
---
<ul>
{entries.map((e) => (
<li>
<time datetime={e.published_at}>{new Date(e.published_at).toLocaleDateString()}</time>
<a href={e.public_url}>{e.title}</a> <small>{e.category}{e.version ? ` · ${e.version}` : ""}</small>
</li>
))}
</ul> 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 failureerror.codeis a stable string such as VALIDATION_ERROR, PLAN_LIMIT_REACHED or RATE_LIMITED, with a remediation message. - POST and PATCH requests accept an
Idempotency-Keyheader; 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.