From 10dd8c89c67acb368026229810a7cb650bbfc645 Mon Sep 17 00:00:00 2001 From: Angel Date: Thu, 6 Aug 2026 09:01:30 -0400 Subject: [PATCH 1/3] feat(konnect): POC for PM-authored changelog with RSS syndication Each changelog entry is a single YAML fragment added by whoever ships the change, mirroring kong-ee's per-PR changelog fragment convention. Renders on-site at /konnect-platform/changelog/ and syndicates the same data as RSS at /konnect-platform/changelog/feed.xml, so external properties can pull from here instead of us pulling from them. --- Makefile | 4 + .../konnect/download-dashboards-as-pdf.yml | 8 + .../konnect/import-dashboards-from-json.yml | 7 + app/_data/schemas/changelog/konnect.json | 24 +++ .../components/konnect_changelog.html | 11 ++ .../components/konnect_changelog.xml | 13 ++ app/_plugins/drops/konnect_changelog.rb | 39 +++++ app/_plugins/tags/konnect_changelog.rb | 35 ++++ app/konnect-platform/changelog-feed.xml | 15 ++ app/konnect-platform/changelog.md | 27 +++ docs/konnect-changelog-entries.md | 29 ++++ tools/changelog-validator/index.js | 60 +++++++ tools/changelog-validator/package-lock.json | 162 ++++++++++++++++++ tools/changelog-validator/package.json | 20 +++ 14 files changed, 454 insertions(+) create mode 100644 app/_data/changelogs/konnect/download-dashboards-as-pdf.yml create mode 100644 app/_data/changelogs/konnect/import-dashboards-from-json.yml create mode 100644 app/_data/schemas/changelog/konnect.json create mode 100644 app/_includes/components/konnect_changelog.html create mode 100644 app/_includes/components/konnect_changelog.xml create mode 100644 app/_plugins/drops/konnect_changelog.rb create mode 100644 app/_plugins/tags/konnect_changelog.rb create mode 100644 app/konnect-platform/changelog-feed.xml create mode 100644 app/konnect-platform/changelog.md create mode 100644 docs/konnect-changelog-entries.md create mode 100644 tools/changelog-validator/index.js create mode 100644 tools/changelog-validator/package-lock.json create mode 100644 tools/changelog-validator/package.json 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..fb389882923 --- /dev/null +++ b/app/konnect-platform/changelog-feed.xml @@ -0,0 +1,15 @@ +--- +layout: null +llm: false +permalink: /konnect-platform/changelog/feed.xml +output_format: rss +--- + + + +{{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..38641964a7a --- /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: /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" + } +} From 5511f499aa2f07c0ac51dac6d28067bf80b565e9 Mon Sep 17 00:00:00 2001 From: Angel Date: Thu, 6 Aug 2026 09:26:26 -0400 Subject: [PATCH 2/3] fix(konnect): use absolute URL for changelog RSS feed link The site's related_resources validation hook requires internal relative links to end with a trailing slash, which the feed's .xml path can't. An absolute URL sidesteps that and is also the right form for an RSS link meant for external consumption anyway. --- app/konnect-platform/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/konnect-platform/changelog.md b/app/konnect-platform/changelog.md index 38641964a7a..dd0fb4f3b47 100644 --- a/app/konnect-platform/changelog.md +++ b/app/konnect-platform/changelog.md @@ -12,7 +12,7 @@ description: "Changelog for {{site.konnect_short_name}}." related_resources: - text: "{{site.konnect_short_name}} changelog RSS feed" - url: /konnect-platform/changelog/feed.xml + url: https://developer.konghq.com/konnect-platform/changelog/feed.xml tags: - changelog From d8bc962170693ba9c4db0ff7c5c34d0515509e43 Mon Sep 17 00:00:00 2001 From: Angel Date: Thu, 6 Aug 2026 09:29:24 -0400 Subject: [PATCH 3/3] fix(konnect): scope changelog RSS feed page to the konnect product Pages without a products: field are dropped entirely by ProductsRenderer during a KONG_PRODUCTS-scoped build (post_read), before rendering even happens. sitemap.xml gets away without one only because it's a root-level page, which this page isn't. --- app/konnect-platform/changelog-feed.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/konnect-platform/changelog-feed.xml b/app/konnect-platform/changelog-feed.xml index fb389882923..da0fd1bf01d 100644 --- a/app/konnect-platform/changelog-feed.xml +++ b/app/konnect-platform/changelog-feed.xml @@ -3,6 +3,8 @@ layout: null llm: false permalink: /konnect-platform/changelog/feed.xml output_format: rss +products: + - konnect ---