diff --git a/Makefile b/Makefile
index 5c4d0bc4146..7a9216da742 100644
--- a/Makefile
+++ b/Makefile
@@ -19,10 +19,14 @@ install:
yarn install --immutable
bundle install
cd tools/frontmatter-validator && npm ci
+ cd tools/changelog-validator && npm ci
validate-frontmatters:
npm --prefix tools/frontmatter-validator run validate
+validate-konnect-changelog:
+ npm --prefix tools/changelog-validator run validate
+
# Using local dependencies, starts a doc site instance on http://localhost:4000.
run: ruby-version-check validate-frontmatters
NODE_OPTIONS="--max_old_space_size=8192" yarn netlify dev --offline --skip-wait-port --internal-disable-edge-functions
diff --git a/app/_data/changelogs/konnect/download-dashboards-as-pdf.yml b/app/_data/changelogs/konnect/download-dashboards-as-pdf.yml
new file mode 100644
index 00000000000..ec2bd0f733d
--- /dev/null
+++ b/app/_data/changelogs/konnect/download-dashboards-as-pdf.yml
@@ -0,0 +1,8 @@
+title: "Download dashboards as PDF"
+date: 2026-07-31
+content: |
+ You can now download any dashboard as a PDF directly from the dashboard menu.
+ The exported PDF is print-friendly and contains a link back to the live dashboard,
+ making it easy to share insights with stakeholders, upstream teams, or leadership
+ who do not have a Konnect account.
+url: https://releases.konghq.com/en/download-dashboards-as-pdf-Nq9NwICE
diff --git a/app/_data/changelogs/konnect/import-dashboards-from-json.yml b/app/_data/changelogs/konnect/import-dashboards-from-json.yml
new file mode 100644
index 00000000000..efc958d4d35
--- /dev/null
+++ b/app/_data/changelogs/konnect/import-dashboards-from-json.yml
@@ -0,0 +1,7 @@
+title: "Import dashboards from JSON in Konnect Analytics"
+date: 2026-07-31
+content: |
+ Importing a dashboard from JSON has always been possible through the API. It's now
+ in the UI too. From Create dashboard, choose Import JSON definition and upload the
+ file from your machine. Konnect validates the file and builds the dashboard for you.
+url: https://releases.konghq.com/en/import-dashboards-from-json-in-konnect-analytics
diff --git a/app/_data/schemas/changelog/konnect.json b/app/_data/schemas/changelog/konnect.json
new file mode 100644
index 00000000000..43f91c7021c
--- /dev/null
+++ b/app/_data/schemas/changelog/konnect.json
@@ -0,0 +1,24 @@
+{
+ "$id": "schema:changelog:konnect",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["title", "date", "content"],
+ "properties": {
+ "title": {
+ "type": "string",
+ "minLength": 1
+ },
+ "date": {
+ "type": "string",
+ "format": "date"
+ },
+ "content": {
+ "type": "string",
+ "minLength": 1
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ }
+ }
+}
diff --git a/app/_includes/components/konnect_changelog.html b/app/_includes/components/konnect_changelog.html
new file mode 100644
index 00000000000..60284e48719
--- /dev/null
+++ b/app/_includes/components/konnect_changelog.html
@@ -0,0 +1,11 @@
+{% for entry in changelog.entries %}
+
{{ entry.title }}
+{{ entry.date | date: "%B %-d, %Y" }}
+
+{{ entry.content | markdownify }}
+
+{% if entry.url %}
+Learn more
+{% endif %}
+
+{% endfor %}
diff --git a/app/_includes/components/konnect_changelog.xml b/app/_includes/components/konnect_changelog.xml
new file mode 100644
index 00000000000..e9ebc9af425
--- /dev/null
+++ b/app/_includes/components/konnect_changelog.xml
@@ -0,0 +1,13 @@
+{% for entry in changelog.entries %}
+-
+
{{ entry.title | xml_escape }}
+{{ entry.date | date_to_rfc822 }}
+{{ entry.content | markdownify | strip_html | normalize_whitespace | xml_escape }}
+{% if entry.url %}
+ {{ entry.url | xml_escape }}
+{{ entry.url | xml_escape }}
+{% else %}
+{{ entry.title | slugify }}-{{ entry.date | date: "%Y-%m-%d" }}
+{% endif %}
+
+{% endfor %}
diff --git a/app/_plugins/drops/konnect_changelog.rb b/app/_plugins/drops/konnect_changelog.rb
new file mode 100644
index 00000000000..03cb0aa4486
--- /dev/null
+++ b/app/_plugins/drops/konnect_changelog.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Jekyll
+ module Drops
+ class KonnectChangelog < Liquid::Drop # rubocop:disable Style/Documentation
+ class Entry < Liquid::Drop # rubocop:disable Style/Documentation
+ attr_reader :title, :date, :content, :url
+
+ def initialize(title:, date:, content:, url: nil) # rubocop:disable Lint/MissingSuper
+ @title = title
+ @date = date
+ @content = content
+ @url = url
+ end
+ end
+
+ def initialize(site:) # rubocop:disable Lint/MissingSuper
+ @site = site
+ end
+
+ def entries
+ @entries ||= raw_entries.map do |data|
+ Entry.new(
+ title: data['title'],
+ date: data['date'],
+ content: data['content'],
+ url: data['url']
+ )
+ end.sort_by(&:date).reverse
+ end
+
+ private
+
+ def raw_entries
+ (@site.data.dig('changelogs', 'konnect') || {}).values
+ end
+ end
+ end
+end
diff --git a/app/_plugins/tags/konnect_changelog.rb b/app/_plugins/tags/konnect_changelog.rb
new file mode 100644
index 00000000000..f7c83b8361d
--- /dev/null
+++ b/app/_plugins/tags/konnect_changelog.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Jekyll
+ class RenderKonnectChangelog < Liquid::Tag # rubocop:disable Style/Documentation
+ def initialize(tag_name, param, _tokens)
+ super
+
+ @param = param.strip
+ end
+
+ def render(context)
+ @context = context
+ @page = @context.environments.first['page']
+ site = context.registers[:site]
+ changelog = Drops::KonnectChangelog.new(site:)
+
+ context.stack do
+ context['changelog'] = changelog
+ Liquid::Template.parse(template, { line_numbers: true }).render(context)
+ end
+ end
+
+ private
+
+ def template
+ if @page['output_format'] == 'rss'
+ File.read(File.expand_path('app/_includes/components/konnect_changelog.xml'))
+ else
+ File.read(File.expand_path('app/_includes/components/konnect_changelog.html'))
+ end
+ end
+ end
+end
+
+Liquid::Template.register_tag('konnect_changelog', Jekyll::RenderKonnectChangelog)
diff --git a/app/konnect-platform/changelog-feed.xml b/app/konnect-platform/changelog-feed.xml
new file mode 100644
index 00000000000..da0fd1bf01d
--- /dev/null
+++ b/app/konnect-platform/changelog-feed.xml
@@ -0,0 +1,17 @@
+---
+layout: null
+llm: false
+permalink: /konnect-platform/changelog/feed.xml
+output_format: rss
+products:
+ - konnect
+---
+
+
+
+{{site.konnect_short_name}} changelog
+ {{site.links.web}}/konnect-platform/changelog/
+Changelog for {{site.konnect_short_name}}.
+{% konnect_changelog %}
+
+
diff --git a/app/konnect-platform/changelog.md b/app/konnect-platform/changelog.md
new file mode 100644
index 00000000000..dd0fb4f3b47
--- /dev/null
+++ b/app/konnect-platform/changelog.md
@@ -0,0 +1,27 @@
+---
+title: "{{site.konnect_short_name}} changelog"
+content_type: reference
+layout: reference
+products:
+ - konnect
+works_on:
+ - konnect
+breadcrumbs:
+ - /konnect/
+description: "Changelog for {{site.konnect_short_name}}."
+
+related_resources:
+ - text: "{{site.konnect_short_name}} changelog RSS feed"
+ url: https://developer.konghq.com/konnect-platform/changelog/feed.xml
+
+tags:
+ - changelog
+
+search_aliases:
+ - release notes
+ - konnect release notes
+ - whats new
+---
+Changelog for {{site.konnect_short_name}}.
+
+{% konnect_changelog %}
diff --git a/docs/konnect-changelog-entries.md b/docs/konnect-changelog-entries.md
new file mode 100644
index 00000000000..c9fbc29d9ca
--- /dev/null
+++ b/docs/konnect-changelog-entries.md
@@ -0,0 +1,29 @@
+# Writing a Konnect changelog entry
+
+Konnect's changelog is authored directly in this repo. Each entry is one YAML file — add yours in the same PR as the change it describes (or right before you ship it), and it publishes automatically on merge.
+
+## Adding an entry
+
+1. Create a new file in `app/_data/changelogs/konnect/`, named after your change, e.g. `download-dashboards-as-pdf.yml`.
+2. Fill it in using this template:
+
+```yaml
+title: "Download dashboards as PDF"
+date: 2026-07-31
+content: |
+ You can now download any dashboard as a PDF directly from the dashboard menu.
+ The exported PDF is print-friendly and contains a link back to the live dashboard.
+url: https://releases.konghq.com/en/download-dashboards-as-pdf-Nq9NwICE
+```
+
+| Field | Required | Notes |
+|---|---|---|
+| `title` | Yes | Short, user-facing summary of the change. |
+| `date` | Yes | `YYYY-MM-DD`. |
+| `content` | Yes | Full description. Markdown is fine. |
+| `url` | No | Link to a demo, related doc, or the original announcement. |
+
+3. Run `make validate-konnect-changelog` locally to catch typos or missing fields before opening your PR.
+4. Open the PR. On merge, the entry renders at `/konnect-platform/changelog/` and in the RSS feed at `/konnect-platform/changelog/feed.xml`.
+
+Do not add a file named `template.yml` (or anything else you don't want published) to `app/_data/changelogs/konnect/` — every file in that directory is loaded and rendered as a real entry.
diff --git a/tools/changelog-validator/index.js b/tools/changelog-validator/index.js
new file mode 100644
index 00000000000..11fb607c2cf
--- /dev/null
+++ b/tools/changelog-validator/index.js
@@ -0,0 +1,60 @@
+import fs from "fs/promises";
+import { readFileSync } from "fs";
+import { glob } from "tinyglobby";
+import yaml from "js-yaml";
+import { fileURLToPath } from "url";
+
+import Ajv from "ajv";
+import addFormats from "ajv-formats";
+
+const ajv = new Ajv({ allErrors: true, strict: false });
+addFormats(ajv);
+
+async function validateChangelogs() {
+ const schema = JSON.parse(
+ readFileSync("../../app/_data/schemas/changelog/konnect.json", "utf-8"),
+ );
+ const validate = ajv.compile(schema);
+
+ const files = await glob(["app/_data/changelogs/konnect/*.{yml,yaml}"], {
+ cwd: "../../",
+ });
+
+ const errors = [];
+ for (const filePath of files) {
+ const file = await fs.readFile(`../../${filePath}`, "utf-8");
+ const data = yaml.load(file);
+
+ // js-yaml parses unquoted ISO dates (e.g. `date: 2026-07-31`) into a
+ // native Date, but the schema validates the YAML source as a string.
+ if (data && data.date instanceof Date) {
+ data.date = data.date.toISOString().slice(0, 10);
+ }
+
+ if (!validate(data)) {
+ errors.push({ filePath, errors: validate.errors });
+ }
+ }
+
+ if (errors.length) {
+ console.log(`Errors: ${errors.length}`);
+ for (const error of errors) {
+ console.log(error.filePath);
+ console.log(JSON.stringify(error.errors, null, 2));
+ }
+ process.exit(1);
+ }
+
+ console.log(`No invalid changelog entries detected (${files.length} checked).`);
+}
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ try {
+ validateChangelogs();
+ } catch (e) {
+ console.log(e);
+ process.exit(1);
+ }
+}
+
+export default validateChangelogs;
diff --git a/tools/changelog-validator/package-lock.json b/tools/changelog-validator/package-lock.json
new file mode 100644
index 00000000000..8596ee15b7f
--- /dev/null
+++ b/tools/changelog-validator/package-lock.json
@@ -0,0 +1,162 @@
+{
+ "name": "changelog-validator",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "changelog-validator",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "js-yaml": "^4.1.0",
+ "tinyglobby": "^0.2.12"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
+ "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ }
+ }
+}
diff --git a/tools/changelog-validator/package.json b/tools/changelog-validator/package.json
new file mode 100644
index 00000000000..4d068e885cb
--- /dev/null
+++ b/tools/changelog-validator/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "changelog-validator",
+ "version": "1.0.0",
+ "description": "Validates Konnect changelog entry fragments against a JSON schema.",
+ "type": "module",
+ "main": "index.js",
+ "scripts": {
+ "validate": "node index.js",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "tinyglobby": "^0.2.12",
+ "js-yaml": "^4.1.0"
+ }
+}