Repository: elastic/kibana-release-notes
Pull request #44: Add support for Markdown templates
Author: login: Ikuni17
Created: 2025-05-01T01:08:58Z | Merged: 2025-05-27T17:04:02Z

## Summary
Closes elastic/kibana-operations#283

- Adds support for Markdown templates and generates Markdown release notes for Serverless and `v9+` of other solutions.
- Removes Security label restrictions on Serverless release notes
- Deprecations and Breaking changes have their own files in the docs system, when we generate here it's all the same file but they are separated at the end.

## Testing
0. Pull repo, `yarn install`, checkout `feature/283/markdown-support`
1. `yarn start`
2. Generate notes!

## Examples
<details>
<summary><h3>Serverless</h3></summary>
<br>

```md
## May 5, 2025 [serverless-changelog-05052025]

### Features and enhancements [serverless-changelog-05052025-features-enhancements]

* Fleet agents tag filter is searchable and sorted [#219639]({{kib-pull}}219639).
* ES|QL rule type improvements - adding grouping per row [#212135]({{kib-pull}}212135).
* Added compact view in monitors overview page [#219060]({{kib-pull}}219060).
* Investigation Guide backend [#216377]({{kib-pull}}216377).
* Adds context.grouping action variable in SLO Burn rate and ES Query rules [#213550]({{kib-pull}}213550).
* 790 deployment environment discrepancy [#217899]({{kib-pull}}217899).
* Adds Edot logging level to central config [#219722]({{kib-pull}}219722).
* Updates kibana MITRE data to `v16.1` [#215026]({{kib-pull}}215026).
* Fixes some OpenAI models not accepting temperature for Inference service [#218887]({{kib-pull}}218887).
* Update styles for Color formatter to look like a badge [#189391]({{kib-pull}}189391).
* Adds the ability to switch between relative and absolute time range [#218056]({{kib-pull}}218056).


### Fixes [serverless-changelog-05052025-fixes]

* Fixes ignored dynamic templates [#219875]({{kib-pull}}219875).
* [ES|QL] Sync the Dashboard ES|QL query and filters with the corresponding in Visualizations in *Lens* [#218997]({{kib-pull}}218997).
* Fixes option list control making 2 requests on refresh [#219625]({{kib-pull}}219625).
* Fixes capability required for Siem Migrations Topic [#219427]({{kib-pull}}219427).
* Monitor status rule - remove return to avoid limiting alerts [#219291]({{kib-pull}}219291).
% !!TODO!! The above PR had a lengthy release note description:
% Synthetics monitor status rule - Ensures that an individual alert is sent per monitor configuration when the "Receive distinct alerts per location" toggle is unchecked.
* Fixed monitor status rule control [#218994]({{kib-pull}}218994).
* Don't open in flyout mode on mount [#219420]({{kib-pull}}219420).
* Change the alerts query to include environment not defined value [#219228]({{kib-pull}}219228).
* Fixes Alerts environment query follow up [#219571]({{kib-pull}}219571).
* Handle missing `trace` in API response [#219512]({{kib-pull}}219512).
* Handle create anomaly detection jobs failures [#219364]({{kib-pull}}219364).
* Optional chaining to prevent undefined error in `custom_link_flyout.tsx` [#219668]({{kib-pull}}219668).
* Query alerts using the `alert.start` field and update alerts function API test to check alert information [#219651]({{kib-pull}}219651).
* Fixes scroll in error flyout [#218697]({{kib-pull}}218697).
* Adding code to check privileges for enabling the Run Engine button [#213054]({{kib-pull}}213054).
* Fixes rule import error message display [#218701]({{kib-pull}}218701).
* Fixes broken icons in integrations coming from Home plugin [#219206]({{kib-pull}}219206).
* Remove check for unused Connector role [#219358]({{kib-pull}}219358).
* Ensure ability to change provider without error [#219020]({{kib-pull}}219020).
* Correct quotes in ES|QL queries for function arguments [#217680]({{kib-pull}}217680).
```
</details>

<details>
<summary><h3>Kibana 9.1.0</h3></summary>
<br>

```md
% FEATURES, ENHANCEMENTS, FIXES
% Paste in index.md

## 9.1.0 [kibana-9.1.0-release-notes]

% ::::{NOTE}
% ::::


### Features and enhancements [kibana-9.1.0-features-enhancements]

**Alerting**:
* Support rrule for task scheduling [#217728]({{kib-pull}}217728).
* Publish new public APIs for the Maintenance Window [#216756]({{kib-pull}}216756).
* Alerts created within a Maintenance Windows trigger actions after the MW expires [#219797]({{kib-pull}}219797).
* Moves rule settings to a flyout instead of a modal [#216162]({{kib-pull}}216162).
* Allow users to delete a snooze schedule from a rule using schedule id [#213247]({{kib-pull}}213247).
* Allow users to create a snooze schedule for rule via API [#210584]({{kib-pull}}210584).

**Dashboards and Visualizations**:
* "Compare to" badge for Metric chart in *Lens* [#214811]({{kib-pull}}214811).
* Enable read only editor mode to inspect panel's configuration in *Lens* [#208554]({{kib-pull}}208554).
* [dashboard] Basic keyboard interaction [#208286]({{kib-pull}}208286).
* Adds ability to open links in *Vega* in new window [#216200]({{kib-pull}}216200).
* Inject / extract tag references [#214788]({{kib-pull}}214788).
* Globe projection [#212437]({{kib-pull}}212437).
* Keeps the chart configuration changes done by the user when changing the query whenever it is possible [#210780]({{kib-pull}}210780).

**Data ingestion and Fleet**:
* Enable feature flag `enableAutomaticAgentUpgrades` [#219932]({{kib-pull}}219932).
* Adds Edit ReadMe Functionality To Custom Integrations [#215259]({{kib-pull}}215259).
% !!TODO!! The above PR had a lengthy release note description:
% Adds edit functionality to custom integrations, allowing a user to edit the README file of a custom integration and save it to be persisted. Additionally, saving will automatically increment the version of the integration and update all associated policies.
* Adds ssl fields to agent binary source settings [#213211]({{kib-pull}}213211).
* Expose ssl options for ES and remote ES outputs in UI [#208745]({{kib-pull}}208745).
* Adds SSL options to fleet server hosts settings [#208091]({{kib-pull}}208091).
* Fleet agents tag filter is searchable and sorted [#219639]({{kib-pull}}219639).
* Callout breaking changes on integration upgrade [#217257]({{kib-pull}}217257).
* Adds msi installer command for fleet server and agents [#217217]({{kib-pull}}217217).
* Optional ssl for fleet logstash output [#216216]({{kib-pull}}216216).
* Format last activity value in fleet agent details view as datetime [#215531]({{kib-pull}}215531).
* Support `searchAfter` and PIT (point-in-time) parameters for get agents list API [#213486]({{kib-pull}}213486).
* Register custom integrations search provider [#213013]({{kib-pull}}213013).

**Discover**:
* Creates control by typing a questionmark [#216839]({{kib-pull}}216839).
* Implementing click action for Stacktrace and Degraded Fields on Discover [#214413]({{kib-pull}}214413).
* Added context aware logic for logs view in discover to show Load More… [#211176]({{kib-pull}}211176).
* Adds an ES|QL control option on the dashboard controls dropdown [#219495]({{kib-pull}}219495).
* Adds 'Copy value' button to field value cells [#218817]({{kib-pull}}218817).
* Enable suggestions for `CHANGE_POINT` command [#218100]({{kib-pull}}218100).
* Autocomplete for `STATS...WHERE` [#216379]({{kib-pull}}216379).
* Validation and autocomplete support for the `CHANGE_POINT` command [#216043]({{kib-pull}}216043).
* Highlights the code examples in our inline docs [#214915]({{kib-pull}}214915).
* Suggest triple quotes when the user selects the `KQL` / `QSTR` [#211457]({{kib-pull}}211457).
* Display a warning and a tooltip for the `_score` column in the grid [#211013]({{kib-pull}}211013).
* Allow command/ctrl click for "New" action in top nav [#210982]({{kib-pull}}210982).
* Apply compact Display options Popover layout [#210180]({{kib-pull}}210180).
* Allows the creation of dynamic aggregations controls for ES|QL charts [#210170]({{kib-pull}}210170).

**Elastic Observability solution**:
For the Elastic Observability 9.1.0 release information, refer to [Elastic Observability Solution Release Notes](docs-content://release-notes/elastic-observability/index.md).

**Elastic Security solution**:
For the Elastic Security 9.1.0 release information, refer to [Elastic Security Solution Release Notes](docs-content://release-notes/elastic-security/index.md).

**Kibana platform**:
* Favorite a dashboard from within [#201596]({{kib-pull}}201596).

**Kibana security**:
* Adds defaultSolution to spaces config [#218360]({{kib-pull}}218360).

**Machine Learning**:
* Moves job and trained model management features into Stack Management [#204290]({{kib-pull}}204290).
* Fixes some OpenAI models not accepting temperature for Inference service [#218887]({{kib-pull}}218887).
* Support local file path for `xpack.productDocBase.artifactRepositoryUrl` [#217046]({{kib-pull}}217046).
* Adds Voyage AI and DeepSeek icons [#216651]({{kib-pull}}216651).
* Anomalies table: Enhances display for anomaly time function values [#216142]({{kib-pull}}216142).
* Trained models: Replace download button by extending deploy action [#205699]({{kib-pull}}205699).

**Management**:
* Remove enablement check in `PUT /api/streams/{id}` for classic streams [#212289]({{kib-pull}}212289).
* Enable `/api/streams/{id}/_group` endpoints for GroupStreams [#210114]({{kib-pull}}210114).
* Update styles for Color formatter to look like a badge [#189391]({{kib-pull}}189391).

**Platform**:
* Added an option to User Settings that allows the Kibana interface to display in a high contrast mode [#216242]({{kib-pull}}216242).

**Sharing**:
* Adds the ability to switch between relative and absolute time range [#218056]({{kib-pull}}218056).
* Adds 'page reload' screen reader warning [#214822]({{kib-pull}}214822).


### Fixes [kibana-9.1.0-fixes]

**Alerting**:
* Fixes ignored dynamic templates [#219875]({{kib-pull}}219875).
* Fixes a regression that caused the cases actions to disappear from the detections engine alerts table bulk actions menu [#215111]({{kib-pull}}215111).

**Dashboards and Visualizations**:
* Allows to retrieve empty columns [#218085]({{kib-pull}}218085).
* Do not break when the table has no data in *Lens* [#217937]({{kib-pull}}217937).
* Fixes coloring/palette assignment on partition charts in *Lens* [#215426]({{kib-pull}}215426).
* Fixes invalid dashboard displayed as 404 instead of showing validation error [#211661]({{kib-pull}}211661).
* Adds `useCustomDragHandle` prop [#210463]({{kib-pull}}210463).
* Fixes presentation panel styles [#210113]({{kib-pull}}210113).
* Store assignments as raw/serialized values [#207957]({{kib-pull}}207957).
% !!TODO!! The above PR had a lengthy release note description:
% This PR fixes an issue ([#193080](https://github.com/elastic/kibana/issues/193080)) where custom ranges and multi-field values were not correctly colored based on selected color mapping configurations.

**Data ingestion and Fleet**:
* Fixes ssl config overridden from advanced yaml in full agent policy [#219902]({{kib-pull}}219902).
* Fixes capability required for Siem Migrations Topic [#219427]({{kib-pull}}219427).
* Make output and fleet server non-editable for agentless policies [#218905]({{kib-pull}}218905).
* Support integrations having secrets with multiple values [#216918]({{kib-pull}}216918).
* Adds remote cluster instructions for syncing integrations [#211997]({{kib-pull}}211997).
* Update install snippets to include all platforms [#210249]({{kib-pull}}210249).

**Discover**:
* Fixes rename wrong validation for asterisc in name [#219832]({{kib-pull}}219832).
* Make icon only presentational [#219696]({{kib-pull}}219696).
* Fixes the wrong source validation in case of unknown patterns [#218352]({{kib-pull}}218352).
* Fixes editor menus on safari [#218167]({{kib-pull}}218167).
* Fixes the broken tooltip suggestions descriptions [#218067]({{kib-pull}}218067).
* Adds items count to fields accordion title aria-label [#216993]({{kib-pull}}216993).
* Discover esc closes flyout when focus is on filter [#216630]({{kib-pull}}216630).
* Enables the timepicker if the time params are used with cast [#215820]({{kib-pull}}215820).
* Calculate the query for retrieving the values correctly [#214905]({{kib-pull}}214905).
* Make sure that the variables in the editor are always up to date [#214833]({{kib-pull}}214833).
* Fixes the visibility of the datepicker [#214728]({{kib-pull}}214728).
* Fixes warnings with escaped quotes [#213685]({{kib-pull}}213685).
* Fixes the wrong validation when a named param is used as function [#213355]({{kib-pull}}213355).
* Supports date_nanos in bucket [#213319]({{kib-pull}}213319).
* Exclude Elasticsearch metadata fields from Display in Content Column [#213255]({{kib-pull}}213255).
* Fixes the suggestion problem in where for multiline queries [#213240]({{kib-pull}}213240).
* Suggest triple quotes when the user selects the `KQL` / `QSTR` [#211457]({{kib-pull}}211457).

**Elastic Observability solution**:
For the Elastic Observability 9.1.0 release information, refer to [Elastic Observability Solution Release Notes](docs-content://release-notes/elastic-observability/index.md).

**Elastic Security solution**:
For the Elastic Security 9.1.0 release information, refer to [Elastic Security Solution Release Notes](docs-content://release-notes/elastic-security/index.md).

**Kibana platform**:
* Remove listing limit warning [#217945]({{kib-pull}}217945).
* Custom link colour option for top banner [#214241]({{kib-pull}}214241).
* Change in sortBy dropdown component option name wording [#206464]({{kib-pull}}206464).

**Machine Learning**:
* Change Point Detection: Fixes 'Use full data' button issue which could cause page to crash [#217291]({{kib-pull}}217291).
* Fixing permission to view ML nodes [#215503]({{kib-pull}}215503).
* Fixes chart in single metric anomaly detection wizard [#214837]({{kib-pull}}214837).
* Trained Models: Prevent multiple clicks in Delete Model dialog [#211580]({{kib-pull}}211580).
* Trained Models: Fixes further deployment of models after a failed deployment [#211459]({{kib-pull}}211459).
* AIOps Hiding saved query controls [#210556]({{kib-pull}}210556).
* Only allow .ndjson files when bulk importing to the knowledge base [#215433]({{kib-pull}}215433).
* Automatic Import now produces correct pipeline to parse CSV files with special characters in column names [#212513]({{kib-pull}}212513).
* Improve finding a function [#210437]({{kib-pull}}210437).

% BREAKING CHANGES
% Paste in breaking-changes.md

## 9.1.0 [kibana-9.1.0-breaking-changes]
$$$kibana-213916$$$
::::{dropdown} Now the fields / functions variables are being described with ?? in the query. The values variables use ? as before 
% **Details**<br> Description
% **Impact**<br> Impact of the breaking change.
% **Action**<br> Steps for mitigating impact.
View [#213916]({{kib-pull}}213916).
::::
```
</details>




Diff:
diff --git a/src/common/github-service.ts b/src/common/github-service.ts
index 98ab747..d04cd92 100644
--- a/src/common/github-service.ts
+++ b/src/common/github-service.ts
@@ -51,7 +51,7 @@ class GitHubService {
   private octokit: Octokit;
   private repoId: number | undefined;
   public repoName: string;
-  public serverlessReleaseDate: string | undefined;
+  public serverlessReleaseDate: Date | undefined;
   public serverlessReleaseTag: string = '';
 
   constructor(config: GitHubServiceConfig) {
@@ -95,7 +95,7 @@ class GitHubService {
     );
 
     return [
-      ...[previousPatch ? [previousPatch.inc('patch')] : []],
+      ...(previousPatch ? [previousPatch.inc('patch')] : []),
       latestVersion.inc('patch').format(),
       latestVersion.inc('minor').format(),
       latestVersion.inc('major').format(),
@@ -315,14 +315,7 @@ class GitHubService {
 
     if (tagForReleaseCommit) {
       this.serverlessReleaseTag = tagForReleaseCommit.name;
-      this.serverlessReleaseDate = new Date(
-        Number(tagForReleaseCommit.name.split('@')[1]) * 1000
-      ).toLocaleDateString('en-US', {
-        year: 'numeric',
-        month: 'long',
-        day: 'numeric',
-        timeZone: 'UTC',
-      });
+      this.serverlessReleaseDate = new Date(Number(tagForReleaseCommit.name.split('@')[1]) * 1000);
     } else {
       throw new Error('No tag found for the release commit');
     }
diff --git a/src/common/output-utils/asciidoc.ts b/src/common/output-utils/asciidoc.ts
deleted file mode 100644
index b9ed86b..0000000
--- a/src/common/output-utils/asciidoc.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import Mustache from 'mustache';
-import { Config } from '../../config';
-import { PrItem } from '../github-service';
-import { extractReleaseNotes, GroupedByArea, NormalizeOptions } from '../pr-utils';
-
-export function renderPageAsAsciidoc(template: string, context: unknown): string {
-  return Mustache.render(template, context);
-}
-
-type PrType = 'feature' | 'enhancement' | 'fix' | 'breaking' | 'deprecation';
-
-function hasTemplate(
-  type: PrType | keyof Config['templates']['prs'],
-  config: Config
-): type is keyof Config['templates']['prs'] {
-  return type in config.templates.prs;
-}
-
-export function renderPrAsAsciidoc(
-  pr: PrItem,
-  type: PrType,
-  config: Config,
-  normalizeOptions?: NormalizeOptions
-): string {
-  const template = hasTemplate(type, config)
-    ? (config.templates.prs[type] as string)
-    : config.templates.prs._other_;
-
-  const releaseNote = extractReleaseNotes(pr, normalizeOptions);
-
-  return Mustache.render(template, {
-    title: releaseNote.title,
-    number: pr.number,
-    details: releaseNote.type === 'releaseNoteDetails' ? releaseNote.details : undefined,
-  });
-}
-
-export function renderGroupedByArea(
-  [areas, unknown]: GroupedByArea,
-  type: PrType,
-  config: Config,
-  version: string
-): string {
-  const output: string[] = [];
-  const sortedAreas = [...config.areas].sort((a, b) => a.title.localeCompare(b.title));
-  for (const area of sortedAreas) {
-    if (areas[area.title]) {
-      output.push(
-        Mustache.render(config.templates.prGroup, {
-          groupTitle: area.title,
-          // If the area has a textOverwriteTemplate render that instead of the list of PRs
-          prs: area.options?.textOverwriteTemplate
-            ? Mustache.render(area.options.textOverwriteTemplate, { version })
-            : areas[area.title]
-                .map((pr) => renderPrAsAsciidoc(pr, type, config, area.options))
-                .join('\n'),
-        })
-      );
-    }
-  }
-  if (unknown.length > 0) {
-    output.push(
-      Mustache.render(config.templates.prGroup, {
-        groupTitle: 'Other',
-        prs: unknown.map((pr) => renderPrAsAsciidoc(pr, type, config)).join('\n'),
-      })
-    );
-  }
-  return output.join('\n');
-}
diff --git a/src/common/output-utils/index.ts b/src/common/output-utils/index.ts
index aab2e9e..dc70a4d 100644
--- a/src/common/output-utils/index.ts
+++ b/src/common/output-utils/index.ts
@@ -1 +1 @@
-export * from './asciidoc';
+export * from './render';
diff --git a/src/common/output-utils/render.ts b/src/common/output-utils/render.ts
new file mode 100644
index 0000000..5aef8b2
--- /dev/null
+++ b/src/common/output-utils/render.ts
@@ -0,0 +1,155 @@
+import Mustache from 'mustache';
+import { Config, OutputTemplateType } from '../../config';
+import { PrItem } from '../github-service';
+import { extractReleaseNotes, GroupedByArea, NormalizeOptions } from '../pr-utils';
+import { groupByArea, groupPrs } from '../../common';
+
+interface RenderPageContext {
+  version: string;
+  minorVersion: string;
+  prs: Record<string, string | unknown>;
+  nextMajorVersion: string;
+  isPatchRelease: boolean;
+  serverlessReleaseDate: string | undefined;
+}
+
+type PrType = 'feature' | 'enhancement' | 'fix' | 'breaking' | 'deprecation';
+
+const getTemplateTypeKey = (isMarkdown: boolean): OutputTemplateType => {
+  return isMarkdown ? 'markdown' : 'asciidoc';
+};
+
+function getPRTemplate(type: PrType, config: Config, isMarkdown: boolean) {
+  const templateTypeKey = getTemplateTypeKey(isMarkdown);
+
+  if (type in config.templates[templateTypeKey].prs) {
+    return config.templates[templateTypeKey].prs[
+      type as keyof typeof config.templates[typeof templateTypeKey]['prs']
+    ] as string;
+  }
+
+  return config.templates[templateTypeKey].prs._other_;
+}
+
+export function renderOutput(
+  config: Config,
+  context: RenderPageContext,
+  isMarkdown: boolean
+): string {
+  const templateTypeKey = getTemplateTypeKey(isMarkdown);
+  const template = context.isPatchRelease
+    ? config.templates[templateTypeKey].pages.patchReleaseNotes ??
+      config.templates[templateTypeKey].pages.releaseNotes
+    : config.templates[templateTypeKey].pages.releaseNotes;
+
+  return Mustache.render(template, context).trim();
+}
+
+function renderPr(
+  pr: PrItem,
+  type: PrType,
+  config: Config,
+  isMarkdown: boolean,
+  normalizeOptions?: NormalizeOptions
+) {
+  const template = getPRTemplate(type, config, isMarkdown);
+  const releaseNote = extractReleaseNotes(pr, normalizeOptions);
+
+  return Mustache.render(template, {
+    title: releaseNote.title,
+    number: pr.number,
+    details: releaseNote.type === 'releaseNoteDetails' ? releaseNote.details : undefined,
+  });
+}
+
+function renderGroupedByArea(
+  [areas, unknown]: GroupedByArea,
+  type: PrType,
+  config: Config,
+  version: string,
+  isMarkdown: boolean
+): string {
+  const output: string[] = [];
+  const sortedAreas = [...config.areas].sort((a, b) => a.title.localeCompare(b.title));
+  const templateTypeKey = getTemplateTypeKey(isMarkdown);
+
+  for (const area of sortedAreas) {
+    if (areas[area.title]) {
+      output.push(
+        Mustache.render(config.templates[templateTypeKey].prGroup, {
+          hasPRGroups: config.areas.length > 1,
+          groupTitle: area.title,
+          // If the area has a textOverwriteTemplate render that instead of the list of PRs
+          prs: area.options?.textOverwriteTemplate
+            ? Mustache.render(area.options.textOverwriteTemplate, { version })
+            : areas[area.title]
+                .map((pr) => renderPr(pr, type, config, isMarkdown, area.options))
+                .join('\n'),
+        })
+      );
+    }
+  }
+
+  if (unknown.length > 0) {
+    output.push(
+      Mustache.render(config.templates[templateTypeKey].prGroup, {
+        groupTitle: 'Other',
+        prs: unknown.map((pr) => renderPr(pr, type, config, isMarkdown)).join('\n'),
+      })
+    );
+  }
+
+  return output.join('\n');
+}
+
+export const getRenderedPRGroups = (
+  prs: PrItem[],
+  config: Config,
+  version: string,
+  isMarkdown: boolean
+) => {
+  const grouped = groupPrs(prs, { includeFeaturesInEnhancements: true });
+
+  const featuresAndEnhancements = isMarkdown
+    ? {
+        featuresAndEnhancements: renderGroupedByArea(
+          groupByArea(grouped.features.concat(grouped.enhancements), config),
+          'feature',
+          config,
+          version,
+          isMarkdown
+        ),
+      }
+    : {
+        features: renderGroupedByArea(
+          groupByArea(grouped.features, config),
+          'feature',
+          config,
+          version,
+          isMarkdown
+        ),
+        enhancements: renderGroupedByArea(
+          groupByArea(grouped.enhancements, config),
+          'enhancement',
+          config,
+          version,
+          isMarkdown
+        ),
+      };
+
+  return {
+    breaking: grouped.breaking.map((pr) => renderPr(pr, 'breaking', config, isMarkdown)).join('\n'),
+    deprecations: grouped.deprecation
+      .map((pr) => renderPr(pr, 'deprecation', config, isMarkdown))
+      .join('\n'),
+    fixes: renderGroupedByArea(
+      groupByArea(grouped.fixes, config),
+      'fix',
+      config,
+      version,
+      isMarkdown
+    ),
+    missingReleaseNoteLabel: grouped.missingLabel,
+    ...featuresAndEnhancements,
+  };
+};
diff --git a/src/config/index.ts b/src/config/index.ts
index 3a0911f..b703bcd 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -1,2 +1,2 @@
 export * from './config';
-export type { TemplateId, Config } from './templates';
+export type { TemplateId, Config, OutputTemplateType } from './templates';
diff --git a/src/config/templates/common.ts b/src/config/templates/common.ts
new file mode 100644
index 0000000..1e49366
--- /dev/null
+++ b/src/config/templates/common.ts
@@ -0,0 +1,352 @@
+import { AreaDefinition } from '.';
+import type { OutputTemplate, OutputTemplateOptions } from './types';
+
+/*
+ * We want this to render as {{str}} in the MD file to allow rendering again later in the docs pipeline.
+ * So we change the tag delimiters temporarily
+ **/
+const createEscapedTag = (str: string) => `{{=<% %>=}}{{${str}}}<%={{ }}=%>`;
+const kibPullTag = createEscapedTag('kib-pull');
+export const kibanaPRMarkdownLink = `[#{{number}}](${kibPullTag}{{number}})`;
+
+const getPatchMarkdownTemplate = (name: string) => {
+  return `% FEATURES, ENHANCEMENTS, FIXES
+% Paste in index.md
+
+## {{version}} [${name}-{{version}}-release-notes]
+
+% ::::{NOTE}
+% ::::
+
+
+{{#prs.featuresAndEnhancements}}
+### Features and enhancements [${name}-{{version}}-features-enhancements]
+{{{prs.featuresAndEnhancements}}}
+{{/prs.featuresAndEnhancements}}
+
+
+{{#prs.fixes}}
+### Fixes [${name}-{{version}}-fixes]
+{{{prs.fixes}}}
+{{/prs.fixes}}`;
+};
+
+const getFullMarkdownTemplate = (name: string, patchTemplate: string) => {
+  return (
+    patchTemplate +
+    `
+
+{{#prs.breaking}}
+% BREAKING CHANGES
+% Paste in breaking-changes.md
+
+## {{version}} [${name}-{{version}}-breaking-changes]
+{{{prs.breaking}}}
+{{/prs.breaking}}
+
+
+{{#prs.deprecations}}
+% DEPRECATIONS
+% Paste in deprecations.md
+
+## {{version}} [${name}-{{version}}-deprecations]
+{{{prs.deprecations}}}
+{{/prs.deprecations}}
+`
+  );
+};
+
+const getBreakingOrDeprecationPRTemplate = (name: string, isBreaking?: boolean) => {
+  return `$$$${name}-{{number}}$$$
+::::{dropdown} {{{title}}} 
+% **Details**<br> Description
+% **Impact**<br> Impact of the ${isBreaking ? 'breaking change' : 'deprecation'}.
+% **Action**<br> Steps for mitigating impact.
+View ${kibanaPRMarkdownLink}.
+::::`;
+};
+
+export const otherPRMarkdownTemplate =
+  `* {{{title}}} ${kibanaPRMarkdownLink}.` +
+  '{{#details}}\n% !!TODO!! The above PR had a lengthy release note description:\n% {{{details}}}{{/details}}';
+
+const dynamicPRGroupTemplate = `{{#hasPRGroups}}\n\n**{{{groupTitle}}}**:\n{{{prs}}}{{/hasPRGroups}}{{^hasPRGroups}}{{{prs}}}{{/hasPRGroups}}`;
+
+export const generateMarkdownTemplate = ({ name }: OutputTemplateOptions): OutputTemplate => {
+  const patchTemplate = getPatchMarkdownTemplate(name);
+
+  return {
+    pages: {
+      releaseNotes: getFullMarkdownTemplate(name, patchTemplate),
+      patchReleaseNotes: patchTemplate,
+    },
+    prs: {
+      breaking: getBreakingOrDeprecationPRTemplate(name, true),
+      deprecation: getBreakingOrDeprecationPRTemplate(name),
+      _other_: otherPRMarkdownTemplate,
+    },
+    prGroup: dynamicPRGroupTemplate,
+  };
+};
+
+export const securityLabels = [
+  'Team:SIEM',
+  'Team:SecuritySolution',
+  'Team: SecuritySolution',
+  'Team:Endpoint Response',
+  'Team:Entity Analytics',
+  'Team:Security Generative AI',
+  'Team:Threat Hunting',
+  'Team:Threat Hunting:Explore',
+  'Team:Threat Hunting:Investigations',
+  'Team:Detections and Resp',
+  'Team:Asset Management',
+  'Team:Onboarding and Lifecycle Mgt',
+  'Team:Security Solution Platform',
+  'Team:Detection Alerts',
+  'Team: CTI',
+  'Team:CTI',
+  'Team:Threat Hunting:Cases',
+  'Team:Cloud Security',
+  'Team:Detection Engine',
+  'Team:Defend Workflows',
+  'Team:Detection Rules',
+  'Team:Security-Scalability',
+  'Feature:Timeline',
+  'Feature:Detection Rules',
+  'Feature:Detection Alerts',
+  'Feature:Entity Analytics',
+  'Feature:Rule Exceptions',
+  'Feature:AutomaticImport',
+];
+
+export const kibanaAreas: AreaDefinition[] = [
+  {
+    title: 'Theme',
+    labels: ['EUI'],
+  },
+  {
+    title: 'Logstash',
+    labels: ['Feature:Logstash Pipelines'],
+  },
+  {
+    title: 'Management',
+    labels: [
+      'Feature:license',
+      'Feature:Console',
+      'Feature:Search Profiler',
+      'Feature:watcher',
+      'Feature:Index Patterns',
+      'Feature:Data Views',
+      'Feature:Kibana Management',
+      'Feature:Dev Tools',
+      'Feature:Inspector',
+      'Feature:Index Management',
+      'Feature:Streams',
+      'Feature:Snapshot and Restore',
+      'Team:Elasticsearch UI',
+      'Team:Stack Management',
+      'Feature:FieldFormatters',
+      'Feature:CCR',
+      'Feature:ILM',
+      'Feature:Transforms',
+      'Feature:Search',
+      'Project:AsyncSearch',
+      'Feature:Upgrade Assistant',
+    ],
+  },
+  {
+    title: 'Monitoring',
+    labels: ['Team:Monitoring', 'Feature:Telemetry', 'Feature:Stack Monitoring'],
+  },
+  {
+    title: 'Operations',
+    labels: ['Team:Operations', 'Feature:License'],
+  },
+  {
+    title: 'Kibana UI',
+    labels: ['Kibana UI', 'Team:Core UI', 'Feature:Header'],
+  },
+  {
+    title: 'Platform',
+    labels: [
+      'Team:Core',
+      'Feature:Plugins',
+      'Feature:New Platform',
+      'Project:i18n',
+      'Feature:ExpressionLanguage',
+      'Feature:Saved Objects',
+      'Team:Stack Services',
+      'Feature:NP Migration',
+      'Feature:Task Manager',
+      'Team:Pulse',
+    ],
+  },
+  {
+    title: 'Machine Learning',
+    labels: [
+      ':ml',
+      'Feature:Anomaly Detection',
+      'Feature:Data Frames',
+      'Feature:File Data Viz',
+      'Feature:ml-results',
+      'Feature:Data Frame Analytics',
+      'Feature:Inference UI',
+    ],
+  },
+  {
+    title: 'Maps',
+    labels: ['Team:Geo'],
+  },
+  {
+    title: 'QA',
+    labels: ['Team:QA'],
+  },
+  {
+    title: 'Kibana security',
+    labels: [
+      'Team:Security',
+      'Feature:Security/Spaces',
+      'Feature:users and roles',
+      'Feature:Security/Authentication',
+      'Feature:Security/Authorization',
+      'Feature:Security/Feature Controls',
+      'Team:Security-Scalability',
+    ],
+  },
+  {
+    title: 'Canvas',
+    labels: ['Feature:Canvas'],
+  },
+  {
+    title: 'Dashboards and Visualizations',
+    labels: [
+      'Feature:Dashboard',
+      'Feature:Drilldowns',
+      'Project:TimeToVisualize',
+      'Team:Presentation',
+      'Feature:Lens',
+      'Feature:Timelion',
+      'Feature:TSVB',
+      'Feature:Coordinate Map',
+      'Feature:Region Map',
+      'Feature:Vega',
+      'Feature:Gauge Vis',
+      'Feature:Tagcloud',
+      'Feature:Vis Loader',
+      'Feature:Vislib',
+      'Feature:Vis Editor',
+      'Feature:Aggregations',
+      'Feature:Input Control',
+      'Feature:Visualizations',
+      'Feature:Markdown',
+      'Feature:Data Table',
+      'Feature:Heatmap',
+      'Feature:Pie Chart',
+      'Feature:XYAxis',
+      'Feature:Graph',
+      'Feature:New Feature',
+      'Feature:MetricVis',
+      'Team:Visualizations',
+    ],
+    options: {
+      bracketHandling: 'visualizations',
+    },
+  },
+  {
+    title: 'Discover',
+    labels: ['Feature:Discover', 'Team:DataDiscovery', 'Team:ESQL'],
+  },
+  {
+    title: 'ES|QL',
+    labels: ['Team:ESQL'],
+  },
+  {
+    title: 'Querying & Filtering',
+    labels: [
+      'Feature:Query Bar',
+      'Feature:Courier',
+      'Feature:Filters',
+      'Feature:Timepicker',
+      'Feature:Highlight',
+      'Feature:KQL',
+      'Feature:Rollups',
+      'Feature:Search',
+      'Project:AsyncSearch',
+    ],
+  },
+  {
+    title: 'Reporting',
+    labels: ['Feature:Reporting', 'Team:Reporting Services'],
+  },
+  {
+    title: 'Sharing',
+    labels: ['Feature:Embedding', 'Feature:SharingURLs'],
+  },
+  {
+    title: 'Elastic Security solution',
+    labels: securityLabels,
+    options: {
+      textOverwriteTemplate:
+        'For the Elastic Security {{version}} release information, refer to [Elastic Security Solution Release Notes](docs-content://release-notes/elastic-security/index.md).',
+    },
+  },
+  {
+    title: 'Code',
+    labels: ['Team:Code'],
+  },
+  {
+    title: 'Elastic Observability solution',
+    labels: [
+      'Feature:Observability Home',
+      'Feature:SLO',
+      'Team:obs-ux-management',
+      'Team:Obs AI Assistant',
+      'Team:obs-ux-infra_services',
+      'Team:obs-ux-logs',
+      'Team:obs-knowledge',
+      'Team:obs-entities',
+      'ci:project-deploy-observability',
+      'Feature:Uptime',
+      'Team:uptime',
+      'Team:apm',
+      'Team:APM',
+      'Feature:Metrics UI',
+      'Team:logs-metrics-ui',
+      'Feature:Logs UI',
+      'Feature:Infra UI',
+      'Feature:Service Maps',
+    ],
+    options: {
+      textOverwriteTemplate:
+        'For the Elastic Observability {{version}} release information, refer to [Elastic Observability Solution Release Notes](docs-content://release-notes/elastic-observability/index.md).',
+    },
+  },
+  {
+    title: 'Beats Management',
+    labels: ['Feature:beats-cm', 'Team:Beats'],
+  },
+  {
+    title: 'Alerting',
+    labels: ['Feature:Alerting', 'Team:Alerting Services', 'Feature:Actions', 'Team:ResponseOps'],
+  },
+  {
+    title: 'Data ingestion and Fleet',
+    labels: ['Team:Fleet', 'Ingest', 'Feature:Ingest Node Pipelines'],
+  },
+  {
+    title: 'Kibana platform',
+    labels: [
+      'Team:SharedUX',
+      'Team:Design',
+      'Project:Accessibility',
+      'Feature:Add Data',
+      'Feature:Home',
+      'Team:Cloud',
+    ],
+  },
+  {
+    title: 'Search',
+    labels: ['Team:Search'],
+  },
+];
diff --git a/src/config/templates/endpoint.ts b/src/config/templates/endpoint.ts
index 9760725..8aa39e0 100644
--- a/src/config/templates/endpoint.ts
+++ b/src/config/templates/endpoint.ts
@@ -1,4 +1,5 @@
 import type { Config } from './types';
+import { generateMarkdownTemplate } from './common';
 
 export const endpointTemplate: Config = {
   repoName: 'endpoint-dev',
@@ -9,8 +10,9 @@ export const endpointTemplate: Config = {
     },
   ],
   templates: {
-    pages: {
-      releaseNotes: `[discrete]
+    asciidoc: {
+      pages: {
+        releaseNotes: `[discrete]
 [[release-notes-{{version}}]]
 === {{version}}
 {{#prs.breaking}}
@@ -50,14 +52,16 @@ export const endpointTemplate: Config = {
 {{/prs.fixes}}
 
 `,
+      },
+      prGroup: '{{{prs}}}',
+      prs: {
+        breaking: `*{{{title}}}*\n\n!!TODO!!\n`,
+        deprecation: `*{{{title}}}*\n\n!!TODO!!\n`,
+        _other_:
+          '* {{{title}}}.' +
+          '{{#details}}\n////\n!!TODO!! The above PR had a lengthy release note description:\n{{{details}}}\n////{{/details}}',
+      },
     },
-    prGroup: '{{{prs}}}',
-    prs: {
-      breaking: `*{{{title}}}*\n\n!!TODO!!\n`,
-      deprecation: `*{{{title}}}*\n\n!!TODO!!\n`,
-      _other_:
-        '* {{{title}}}.' +
-        '{{#details}}\n////\n!!TODO!! The above PR had a lengthy release note description:\n{{{details}}}\n////{{/details}}',
-    },
+    markdown: generateMarkdownTemplate({ name: 'elastic-security' }),
   },
 };
diff --git a/src/config/templates/kibana.ts b/src/config/templates/kibana.ts
index 83012f3..4b228e1 100644
--- a/src/config/templates/kibana.ts
+++ b/src/config/templates/kibana.ts
@@ -1,240 +1,14 @@
 import type { Config } from './types';
-import { securityLabels } from './security';
+import { generateMarkdownTemplate, kibanaAreas } from './common';
 
 export const kibanaTemplate: Config = {
   repoName: 'kibana',
   excludedLabels: ['release_note:skip', 'Team:Docs', 'reverted', 'backport'],
-  areas: [
-    {
-      title: 'Logstash',
-      labels: ['Feature:Logstash Pipelines'],
-    },
-    {
-      title: 'Management',
-      labels: [
-        'Feature:license',
-        'Feature:Console',
-        'Feature:Search Profiler',
-        'Feature:watcher',
-        'Feature:Index Patterns',
-        'Feature:Data Views',
-        'Feature:Kibana Management',
-        'Feature:Dev Tools',
-        'Feature:Inspector',
-        'Feature:Index Management',
-        'Feature:Snapshot and Restore',
-        'Team:Elasticsearch UI',
-        'Team:Stack Management',
-        'Feature:FieldFormatters',
-        'Feature:CCR',
-        'Feature:ILM',
-        'Feature:Transforms',
-        'Feature:Search',
-        'Project:AsyncSearch',
-        'Feature:Upgrade Assistant',
-      ],
-    },
-    {
-      title: 'Monitoring',
-      labels: ['Team:Monitoring', 'Feature:Telemetry', 'Feature:Stack Monitoring'],
-    },
-    {
-      title: 'Operations',
-      labels: ['Team:Operations', 'Feature:License'],
-    },
-    {
-      title: 'Kibana UI',
-      labels: ['Kibana UI', 'Team:Core UI', 'Feature:Header'],
-    },
-    {
-      title: 'Platform',
-      labels: [
-        'Team:Core',
-        'Feature:Plugins',
-        'Feature:New Platform',
-        'Project:i18n',
-        'Feature:ExpressionLanguage',
-        'Feature:Saved Objects',
-        'Team:Stack Services',
-        'Feature:NP Migration',
-        'Feature:Task Manager',
-        'Team:Pulse',
-      ],
-    },
-    {
-      title: 'Machine Learning',
-      labels: [
-        ':ml',
-        'Feature:Anomaly Detection',
-        'Feature:Data Frames',
-        'Feature:File Data Viz',
-        'Feature:ml-results',
-        'Feature:Data Frame Analytics',
-      ],
-    },
-    {
-      title: 'Maps',
-      labels: ['Team:Geo'],
-    },
-    {
-      title: 'QA',
-      labels: ['Team:QA'],
-    },
-    {
-      title: 'Kibana security',
-      labels: [
-        'Team:Security',
-        'Feature:Security/Spaces',
-        'Feature:users and roles',
-        'Feature:Security/Authentication',
-        'Feature:Security/Authorization',
-        'Feature:Security/Feature Controls',
-        'Team:Security-Scalability',
-      ],
-    },
-    {
-      title: 'Canvas',
-      labels: ['Feature:Canvas'],
-    },
-    {
-      title: 'Dashboards and Visualizations',
-      labels: [
-        'Feature:Dashboard',
-        'Feature:Drilldowns',
-        'Project:TimeToVisualize',
-        'Team:Presentation',
-        'Feature:Lens',
-        'Feature:Timelion',
-        'Feature:TSVB',
-        'Feature:Coordinate Map',
-        'Feature:Region Map',
-        'Feature:Vega',
-        'Feature:Gauge Vis',
-        'Feature:Tagcloud',
-        'Feature:Vis Loader',
-        'Feature:Vislib',
-        'Feature:Vis Editor',
-        'Feature:Aggregations',
-        'Feature:Input Control',
-        'Feature:Visualizations',
-        'Feature:Markdown',
-        'Feature:Data Table',
-        'Feature:Heatmap',
-        'Feature:Pie Chart',
-        'Feature:XYAxis',
-        'Feature:Graph',
-        'Feature:New Feature',
-        'Feature:MetricVis',
-        'Team:Visualizations',
-      ],
-      options: {
-        bracketHandling: 'visualizations',
-      },
-    },
-    {
-      title: 'Discover',
-      labels: ['Feature:Discover', 'Team:DataDiscovery', 'Team:ESQL'],
-    },
-    {
-      title: 'Querying & Filtering',
-      labels: [
-        'Feature:Query Bar',
-        'Feature:Courier',
-        'Feature:Filters',
-        'Feature:Timepicker',
-        'Feature:Highlight',
-        'Feature:KQL',
-        'Feature:Rollups',
-        'Feature:Search',
-        'Project:AsyncSearch',
-      ],
-    },
-    {
-      title: 'Reporting',
-      labels: ['Feature:Reporting', 'Team:Reporting Services'],
-    },
-    {
-      title: 'Sharing',
-      labels: ['Feature:Embedding', 'Feature:SharingURLs'],
-    },
-    {
-      title: 'Elastic Security solution',
-      labels: securityLabels,
-      options: {
-        textOverwriteTemplate:
-          'For the Elastic Security {{version}} release information, refer to {security-guide}/release-notes.html[_Elastic Security Solution Release Notes_].',
-      },
-    },
-    {
-      title: 'Code',
-      labels: ['Team:Code'],
-    },
-    {
-      title: 'Elastic Observability Solution',
-      labels: [
-        'Feature:Observability Home',
-        'Feature:SLO',
-        'Team:obs-ux-management',
-        'Team:Obs AI Assistant',
-        'Team:obs-ux-infra_services',
-        'Team:obs-ux-logs',
-        'Team:obs-knowledge',
-        'Team:obs-entities',
-        'ci:project-deploy-observability',
-      ],
-    },
-    {
-      title: 'Infrastructure',
-      labels: ['Feature:Infra UI', 'Feature:Service Maps'],
-    },
-    {
-      title: 'Logs',
-      labels: ['Feature:Logs UI'],
-    },
-    {
-      title: 'Uptime',
-      labels: ['Feature:Uptime', 'Team:uptime'],
-    },
-    {
-      title: 'Beats Management',
-      labels: ['Feature:beats-cm', 'Team:Beats'],
-    },
-    {
-      title: 'APM',
-      labels: ['Team:apm', 'Team:APM'],
-      priority: 100,
-    },
-    {
-      title: 'Alerting',
-      labels: ['Feature:Alerting', 'Team:Alerting Services', 'Feature:Actions', 'Team:ResponseOps'],
-    },
-    {
-      title: 'Metrics',
-      labels: ['Feature:Metrics UI', 'Team:logs-metrics-ui'],
-    },
-    {
-      title: 'Data ingestion and Fleet',
-      labels: ['Team:Fleet', 'Ingest', 'Feature:Ingest Node Pipelines'],
-    },
-    {
-      title: 'Kibana platform',
-      labels: [
-        'Team:SharedUX',
-        'Team:Design',
-        'Project:Accessibility',
-        'Feature:Add Data',
-        'Feature:Home',
-        'Team:Cloud',
-      ],
-    },
-    {
-      title: 'Elastic Search solution',
-      labels: ['Team:Search'],
-    },
-  ],
+  areas: kibanaAreas,
   templates: {
-    pages: {
-      releaseNotes: `[[release-notes-{{version}}]]
+    asciidoc: {
+      pages: {
+        releaseNotes: `[[release-notes-{{version}}]]
 == {kib} {{version}}
 
 coming::[{{version}}]
@@ -306,7 +80,7 @@ For detailed information about the {{version}} release, review the enhancements
 {{{prs.fixes}}}
 {{/prs.fixes}}
       `,
-      patchReleaseNotes: `[[release-notes-{{version}}]]
+        patchReleaseNotes: `[[release-notes-{{version}}]]
 == {kib} {{version}}
 
 The {{version}} release includes the following fixes.
@@ -325,9 +99,9 @@ The {{version}} release includes the following fixes.
 {{{prs.fixes}}}
 {{/prs.fixes}}
       `,
-    },
-    prs: {
-      breaking: `[discrete]
+      },
+      prs: {
+        breaking: `[discrete]
 [[breaking-{{number}}]]
 * {{{title}}}.
 [%collapsible]
@@ -341,7 +115,7 @@ The {{version}} release includes the following fixes.
 View ({kibana-pull}{{number}}[#{{number}}])
 ====
       `,
-      deprecation: `[discrete]
+        deprecation: `[discrete]
 [[deprecation-{{number}}]]
 * {{{title}}}.
 [%collapsible]
@@ -355,10 +129,12 @@ View ({kibana-pull}{{number}}[#{{number}}])
 View ({kibana-pull}{{number}}[#{{number}}])
 ====
       `,
-      _other_:
-        '* {{{title}}} ({kibana-pull}{{number}}[#{{number}}]).' +
-        '{{#details}}\n////\n!!TODO!! The above PR had a lengthy release note description:\n{{{details}}}\n////{{/details}}',
+        _other_:
+          '* {{{title}}} ({kibana-pull}{{number}}[#{{number}}]).' +
+          '{{#details}}\n////\n!!TODO!! The above PR had a lengthy release note description:\n{{{details}}}\n////{{/details}}',
+      },
+      prGroup: `{{{groupTitle}}}::\n{{{prs}}}`,
     },
-    prGroup: `{{{groupTitle}}}::\n{{{prs}}}`,
+    markdown: generateMarkdownTemplate({ name: 'kibana' }),
   },
 } as const;
diff --git a/src/config/templates/security.ts b/src/config/templates/security.ts
index d0d19f8..db588d5 100644
--- a/src/config/templates/security.ts
+++ b/src/config/templates/security.ts
@@ -1,35 +1,5 @@
 import type { Config } from './types';
-
-export const securityLabels = [
-  'Team:SIEM',
-  'Team:SecuritySolution',
-  'Team: SecuritySolution',
-  'Team:Endpoint Response',
-  'Team:Entity Analytics',
-  'Team:Security Generative AI',
-  'Team:Threat Hunting',
-  'Team:Threat Hunting:Explore',
-  'Team:Threat Hunting:Investigations',
-  'Team:Detections and Resp',
-  'Team:Asset Management',
-  'Team:Onboarding and Lifecycle Mgt',
-  'Team:Security Solution Platform',
-  'Team:Detection Alerts',
-  'Team: CTI',
-  'Team:CTI',
-  'Team:Threat Hunting:Cases',
-  'Team:Cloud Security',
-  'Team:Detection Engine',
-  'Team:Defend Workflows',
-  'Team:Detection Rules',
-  'Team:Security-Scalability',
-  'Feature:Timeline',
-  'Feature:Detection Rules',
-  'Feature:Detection Alerts',
-  'Feature:Entity Analytics',
-  'Feature:Rule Exceptions',
-  'Feature:AutomaticImport',
-];
+import { generateMarkdownTemplate, securityLabels } from './common';
 
 export const securityTemplate: Config = {
   repoName: 'kibana',
@@ -42,8 +12,9 @@ export const securityTemplate: Config = {
     },
   ],
   templates: {
-    pages: {
-      releaseNotes: `[discrete]
+    asciidoc: {
+      pages: {
+        releaseNotes: `[discrete]
 [[release-notes-{{version}}]]
 === {{version}}
 {{#prs.breaking}}
@@ -83,14 +54,16 @@ export const securityTemplate: Config = {
 {{/prs.fixes}}
 
 `,
+      },
+      prGroup: '{{{prs}}}',
+      prs: {
+        breaking: `*{{{title}}}*\n\n!!TODO!!\n\nSee ({kibana-pull}{{number}}[#{{number}}]) for details.\n`,
+        deprecation: `*{{{title}}}*\n\n!!TODO!!\n\nSee ({kibana-pull}{{number}}[#{{number}}]) for details.\n`,
+        _other_:
+          '* {{{title}}} ({kibana-pull}{{number}}[#{{number}}]).' +
+          '{{#details}}\n////\n!!TODO!! The above PR had a lengthy release note description:\n{{{details}}}\n////{{/details}}',
+      },
     },
-    prGroup: '{{{prs}}}',
-    prs: {
-      breaking: `*{{{title}}}*\n\n!!TODO!!\n\nSee ({kibana-pull}{{number}}[#{{number}}]) for details.\n`,
-      deprecation: `*{{{title}}}*\n\n!!TODO!!\n\nSee ({kibana-pull}{{number}}[#{{number}}]) for details.\n`,
-      _other_:
-        '* {{{title}}} ({kibana-pull}{{number}}[#{{number}}]).' +
-        '{{#details}}\n////\n!!TODO!! The above PR had a lengthy release note description:\n{{{details}}}\n////{{/details}}',
-    },
+    markdown: generateMarkdownTemplate({ name: 'elastic-security' }),
   },
 };
diff --git a/src/config/templates/serverless.ts b/src/config/templates/serverless.ts
index c6941b2..c4d78fb 100644
--- a/src/config/templates/serverless.ts
+++ b/src/config/templates/serverless.ts
@@ -1,25 +1,50 @@
 import type { Config } from './types';
+import { kibanaAreas, kibanaPRMarkdownLink, otherPRMarkdownTemplate } from './common';
 
-export const serverlessLabels = [
-  'Team:SecuritySolution',
-  'Team: SecuritySolution',
-  'serverless-bugfix',
-  'serverless-enhancement',
-];
+const serverlessBreakingOrDeprecationTemplate = `* {{{title}}} For more information, refer to ${kibanaPRMarkdownLink}.\n`;
+const serverlessReleaseNotesTemplate = `## {{serverlessReleaseDate}} [serverless-changelog-{{version}}]
+
+{{#prs.featuresAndEnhancements}}
+### Features and enhancements [serverless-changelog-{{version}}-features-enhancements]
+
+{{{prs.featuresAndEnhancements}}}
+{{/prs.featuresAndEnhancements}}
+
+
+{{#prs.fixes}}
+### Fixes [serverless-changelog-{{version}}-fixes]
+
+{{{prs.fixes}}}
+{{/prs.fixes}}
+
+
+{{#prs.breaking}}
+# {{serverlessReleaseDate}} [elastic-cloud-serverless-{{version}}-breaking]
+{{{prs.breaking}}}
+{{/prs.breaking}}
+
+
+{{#prs.deprecations}}
+# {{serverlessReleaseDate}} [elastic-cloud-serverless-{{version}}-deprecations]
+{{{prs.deprecations}}}
+{{/prs.deprecations}}
+`;
 
 export const serverlessTemplate: Config = {
   repoName: 'kibana',
-  includedLabels: serverlessLabels,
   excludedLabels: ['backport', 'release_note:skip', 'reverted'],
-  areas: [
-    {
-      title: 'Elastic Security',
-      labels: serverlessLabels,
-    },
-  ],
+  areas: kibanaAreas.map((area) => {
+    // Remove the textOverwriteTemplate option from Security and Observability solutions
+    if (area.options && 'textOverwriteTemplate' in area.options) {
+      const { textOverwriteTemplate, ...restOptions } = area.options;
+      return { ...area, options: restOptions };
+    }
+    return area;
+  }),
   templates: {
-    pages: {
-      releaseNotes: `[discrete]
+    asciidoc: {
+      pages: {
+        releaseNotes: `[discrete]
 [[release-notes-{{version}}]]
 === {{serverlessReleaseDate}}
 {{#prs.breaking}}
@@ -59,14 +84,26 @@ export const serverlessTemplate: Config = {
 {{/prs.fixes}}
 
 `,
+      },
+      prGroup: '{{{prs}}}',
+      prs: {
+        breaking: `*{{{title}}}*\n\n!!TODO!!\n\nSee ({kibana-pull}{{number}}[#{{number}}]) for details.\n`,
+        deprecation: `*{{{title}}}*\n\n!!TODO!!\n\nSee ({kibana-pull}{{number}}[#{{number}}]) for details.\n`,
+        _other_:
+          '* {{{title}}} ({kibana-pull}{{number}}[#{{number}}]).' +
+          '{{#details}}\n////\n!!TODO!! The above PR had a lengthy release note description:\n{{{details}}}\n////{{/details}}',
+      },
     },
-    prGroup: '{{{prs}}}',
-    prs: {
-      breaking: `*{{{title}}}*\n\n!!TODO!!\n\nSee ({kibana-pull}{{number}}[#{{number}}]) for details.\n`,
-      deprecation: `*{{{title}}}*\n\n!!TODO!!\n\nSee ({kibana-pull}{{number}}[#{{number}}]) for details.\n`,
-      _other_:
-        '* {{{title}}} ({kibana-pull}{{number}}[#{{number}}]).' +
-        '{{#details}}\n////\n!!TODO!! The above PR had a lengthy release note description:\n{{{details}}}\n////{{/details}}',
+    markdown: {
+      pages: {
+        releaseNotes: serverlessReleaseNotesTemplate,
+      },
+      prGroup: '{{{prs}}}',
+      prs: {
+        breaking: serverlessBreakingOrDeprecationTemplate,
+        deprecation: serverlessBreakingOrDeprecationTemplate,
+        _other_: otherPRMarkdownTemplate,
+      },
     },
   },
 };
diff --git a/src/config/templates/types.ts b/src/config/templates/types.ts
index 348c455..8cf3b45 100644
--- a/src/config/templates/types.ts
+++ b/src/config/templates/types.ts
@@ -1,4 +1,4 @@
-interface AreaDefinition {
+export interface AreaDefinition {
   title: string;
   labels?: readonly string[];
   /**
@@ -12,6 +12,21 @@ interface AreaDefinition {
   };
 }
 
+export interface OutputTemplate {
+  pages: {
+    releaseNotes: string;
+    patchReleaseNotes?: string;
+  };
+  prs: {
+    breaking?: string;
+    deprecation?: string;
+    _other_: string;
+  };
+  prGroup: string;
+}
+
+export type OutputTemplateType = 'markdown' | 'asciidoc';
+
 export interface Config {
   /**
    * A list of exclude labels. PRs having any of this label won't be rendered into the release note.
@@ -22,17 +37,10 @@ export interface Config {
    */
   includedLabels?: readonly string[];
   areas: readonly AreaDefinition[];
-  templates: {
-    pages: {
-      releaseNotes: string;
-      patchReleaseNotes?: string;
-    };
-    prs: {
-      breaking?: string;
-      deprecation?: string;
-      _other_: string;
-    };
-    prGroup: string;
-  };
+  templates: Record<OutputTemplateType, OutputTemplate>;
   repoName: string;
 }
+
+export interface OutputTemplateOptions {
+  name: string;
+}
diff --git a/src/pages/release-notes/release-note-output.tsx b/src/pages/release-notes/release-note-output.tsx
index 1724279..3533c2c 100644
--- a/src/pages/release-notes/release-note-output.tsx
+++ b/src/pages/release-notes/release-note-output.tsx
@@ -1,12 +1,8 @@
 import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCode } from '@elastic/eui';
 import { FC, useMemo } from 'react';
-import { groupByArea, groupPrs, PrItem, useGitHubService } from '../../common';
+import { PrItem, useGitHubService } from '../../common';
 import semver from 'semver';
-import {
-  renderGroupedByArea,
-  renderPageAsAsciidoc,
-  renderPrAsAsciidoc,
-} from '../../common/output-utils';
+import { renderOutput, getRenderedPRGroups } from '../../common/output-utils';
 import { useActiveConfig } from '../../config';
 import MonacoEditor from '@monaco-editor/react';
 
@@ -19,52 +15,48 @@ export const ReleaseNoteOutput: FC<Props> = ({ prs, version: ver }) => {
   const [github] = useGitHubService();
   const config = useActiveConfig();
   const isServerless = ver === 'serverless';
-  const version = isServerless ? github.serverlessReleaseTag : ver.replace(/^v(.*)$/, '$1');
+  const version = isServerless
+    ? github.serverlessReleaseDate
+        ?.toLocaleDateString('en-US', {
+          year: 'numeric',
+          month: '2-digit',
+          day: '2-digit',
+          timeZone: 'UTC',
+        })
+        .replace(/\//g, '') ?? ''
+    : ver.replace(/^v(.*)$/, '$1');
   const isPatchVersion = isServerless ? false : semver.patch(version) !== 0;
+  // Docs output changed from ascii to markdown in 9.0.0
+  const isMarkdown = isServerless || semver.gte(ver, '9.0.0');
 
-  const renderedGroups = useMemo(() => {
-    const grouped = groupPrs(prs, { includeFeaturesInEnhancements: true });
-    return {
-      breaking: grouped.breaking.map((pr) => renderPrAsAsciidoc(pr, 'breaking', config)).join('\n'),
-      deprecations: grouped.deprecation
-        .map((pr) => renderPrAsAsciidoc(pr, 'deprecation', config))
-        .join('\n'),
-      fixes: renderGroupedByArea(groupByArea(grouped.fixes, config), 'fix', config, version),
-      features: renderGroupedByArea(
-        groupByArea(grouped.features, config),
-        'feature',
-        config,
-        version
-      ),
-      enhancements: renderGroupedByArea(
-        groupByArea(grouped.enhancements, config),
-        'enhancement',
-        config,
-        version
-      ),
-      missingReleaseNoteLabel: grouped.missingLabel,
-    };
-  }, [config, prs, version]);
+  const renderedGroups = useMemo(
+    () => getRenderedPRGroups(prs, config, version, isMarkdown),
+    [config, prs, version, isMarkdown]
+  );
 
-  const asciidoc = useMemo(
+  const renderedOutput = useMemo(
     () =>
-      renderPageAsAsciidoc(
-        isPatchVersion
-          ? config.templates.pages.patchReleaseNotes ?? config.templates.pages.releaseNotes
-          : config.templates.pages.releaseNotes,
+      renderOutput(
+        config,
         {
           version,
           minorVersion: version.replace(/\.\d+$/, ''),
           prs: renderedGroups,
           nextMajorVersion: isServerless ? '' : `${semver.major(version) + 1}.0.0`,
           isPatchRelease: isPatchVersion,
-          serverlessReleaseDate: github.serverlessReleaseDate,
-        }
-      ).trim(),
+          serverlessReleaseDate: github.serverlessReleaseDate?.toLocaleDateString('en-US', {
+            year: 'numeric',
+            month: 'long',
+            day: 'numeric',
+            timeZone: 'UTC',
+          }),
+        },
+        isMarkdown
+      ),
     [
-      config.templates.pages.patchReleaseNotes,
-      config.templates.pages.releaseNotes,
+      config,
       github.serverlessReleaseDate,
+      isMarkdown,
       isPatchVersion,
       isServerless,
       renderedGroups,
@@ -89,7 +81,12 @@ export const ReleaseNoteOutput: FC<Props> = ({ prs, version: ver }) => {
         </EuiFlexItem>
       )}
       <EuiFlexItem>
-        <MonacoEditor height="100%" options={{ readOnly: true }} value={asciidoc} />
+        <MonacoEditor
+          height="100%"
+          options={{ readOnly: true }}
+          value={renderedOutput}
+          language={isMarkdown ? 'markdown' : undefined}
+        />
       </EuiFlexItem>
     </EuiFlexGroup>
   );


Reviews:
user: login: delanni
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:38:45Z
user: login: florent-leborgne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:53:45Z
user: login: florent-leborgne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T15:59:50Z
user: login: Ikuni17
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:31:30Z
user: login: Ikuni17
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:32:54Z
user: login: Ikuni17
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:33:22Z
user: login: Ikuni17
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T20:55:54Z
user: login: Ikuni17
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T21:08:50Z
user: login: florent-leborgne
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T07:21:21Z
user: login: delanni
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-07T16:20:31Z
user: login: florent-leborgne
body: This LGTM! Thanks for adding all of the requested changes they will save us a lot of time :) 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-26T07:10:08Z

Review comments:
diff_hunk: @@ -25,6 +25,6 @@
     "src"
   ],
   "exclude": [
-    "node_modules/@elastic/eui/src"
+    "node_modules/@elastic/eui/src",
path: tsconfig.json
user: login: delanni
body: nit: this might break a regular `JSON.parse` - if not needed, let's keep the file compatible
```suggestion
    "node_modules/@elastic/eui/src"
```
created_at: 2025-05-06T15:38:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,381 @@
+import { AreaDefinition } from '.';
+import type { OutputTemplate, OutputTemplateOptions } from './types';
+
+const createEscapedTag = (str: string) => `{{=<% %>=}}{{${str}}}<%={{ }}=%>`;
+export const kibPullTag = createEscapedTag('kib-pull');
+
+export const generateMarkdownTemplate = ({
+  name,
+  navigationTitle,
+  templateNameTag = name,
+  urlPath = name,
+}: OutputTemplateOptions): OutputTemplate => {
+  // We want this to render as {{kib}} etc in the MD file, so we change the tag delimiters temporarily
+  const escapedTemplateNameTag = createEscapedTag(templateNameTag);
+
+  const patchTemplate = `---
+navigation_title: "${navigationTitle}"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/release-notes.html
+  - https://www.elastic.co/guide/en/${urlPath}/current/whats-new.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/release-notes-{{version}}.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/enhancements-and-bug-fixes-v{{version}}.html
+---
+
+# ${escapedTemplateNameTag} release notes [${name}-release-notes]
+
+Review the changes, fixes, and more in each version of ${escapedTemplateNameTag}.
+
+To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
+
+% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
+
+% ## version.next [${name}-next-release-notes]
+
+% ### Features and enhancements [${name}-next-features-enhancements]
+% *
+
+% ### Fixes [${name}-next-fixes]
+% *
+
+## {{version}} [${name}-{{versionWithoutPeriods}}-release-notes]
path: src/config/templates/common.ts
user: login: florent-leborgne
body: It should just be {{version}} in all of those IDs instead of {{versionWithoutPeriods}} as per our guidelines. See https://github.com/elastic/docs-content/issues/1152
created_at: 2025-05-06T15:53:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,381 @@
+import { AreaDefinition } from '.';
+import type { OutputTemplate, OutputTemplateOptions } from './types';
+
+const createEscapedTag = (str: string) => `{{=<% %>=}}{{${str}}}<%={{ }}=%>`;
+export const kibPullTag = createEscapedTag('kib-pull');
+
+export const generateMarkdownTemplate = ({
+  name,
+  navigationTitle,
+  templateNameTag = name,
+  urlPath = name,
+}: OutputTemplateOptions): OutputTemplate => {
+  // We want this to render as {{kib}} etc in the MD file, so we change the tag delimiters temporarily
+  const escapedTemplateNameTag = createEscapedTag(templateNameTag);
+
+  const patchTemplate = `---
+navigation_title: "${navigationTitle}"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/release-notes.html
+  - https://www.elastic.co/guide/en/${urlPath}/current/whats-new.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/release-notes-{{version}}.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/enhancements-and-bug-fixes-v{{version}}.html
+---
+
+# ${escapedTemplateNameTag} release notes [${name}-release-notes]
+
+Review the changes, fixes, and more in each version of ${escapedTemplateNameTag}.
+
+To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
+
+% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
+
+% ## version.next [${name}-next-release-notes]
+
+% ### Features and enhancements [${name}-next-features-enhancements]
+% *
+
+% ### Fixes [${name}-next-fixes]
+% *
+
+## {{version}} [${name}-{{versionWithoutPeriods}}-release-notes]
+
+::::{NOTE}
+
+::::
+
+
+{{#prs.enhancements}}
+{{#prs.features}}
+### Features and enhancements [${name}-{{versionWithoutPeriods}}-features-enhancements]
+{{{prs.features}}}
+{{/prs.features}}
+{{{prs.enhancements}}}
+{{/prs.enhancements}}
+
+
+{{#prs.fixes}}
+### Fixes [${name}-{{versionWithoutPeriods}}-fixes]
+{{{prs.fixes}}}
+{{/prs.fixes}}`;
+
+  return {
+    pages: {
+      releaseNotes:
+        patchTemplate +
+        `
+
+{{#prs.breaking}}
+---
+navigation_title: "Breaking changes"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/breaking-changes-summary.html
+---
+# ${escapedTemplateNameTag} breaking changes [${name}-breaking-changes]
+Breaking changes can impact your Elastic applications, potentially disrupting normal operations. Before you upgrade, carefully review the ${escapedTemplateNameTag} breaking changes and take the necessary steps to mitigate any issues. To learn how to upgrade, check [Upgrade](/deploy-manage/upgrade.md).
path: src/config/templates/common.ts
user: login: florent-leborgne
body: Since we're always going to be pasting into the same files cumulatively, no need to regenerate the start of the files each time. IMO

I've been using this trimmed down version for the past 2 releases (for Kibana) and it's fine if you want to take a look: https://github.com/elastic/kibana-release-notes/pull/43/files#diff-7a2f2afb039ab3adb1760e1583fcd43fcc0d529866eb48e4a07fa6c8c438949eR301-R305

The only thing that's not ideal in my template above is that enhancements are after features instead of grouped together properly, in case you have ideas around making this a unified list :D
created_at: 2025-05-06T15:59:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,381 @@
+import { AreaDefinition } from '.';
+import type { OutputTemplate, OutputTemplateOptions } from './types';
+
+const createEscapedTag = (str: string) => `{{=<% %>=}}{{${str}}}<%={{ }}=%>`;
+export const kibPullTag = createEscapedTag('kib-pull');
+
+export const generateMarkdownTemplate = ({
+  name,
+  navigationTitle,
+  templateNameTag = name,
+  urlPath = name,
+}: OutputTemplateOptions): OutputTemplate => {
+  // We want this to render as {{kib}} etc in the MD file, so we change the tag delimiters temporarily
+  const escapedTemplateNameTag = createEscapedTag(templateNameTag);
+
+  const patchTemplate = `---
+navigation_title: "${navigationTitle}"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/release-notes.html
+  - https://www.elastic.co/guide/en/${urlPath}/current/whats-new.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/release-notes-{{version}}.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/enhancements-and-bug-fixes-v{{version}}.html
+---
+
+# ${escapedTemplateNameTag} release notes [${name}-release-notes]
+
+Review the changes, fixes, and more in each version of ${escapedTemplateNameTag}.
+
+To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
+
+% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
+
+% ## version.next [${name}-next-release-notes]
+
+% ### Features and enhancements [${name}-next-features-enhancements]
+% *
+
+% ### Fixes [${name}-next-fixes]
+% *
+
+## {{version}} [${name}-{{versionWithoutPeriods}}-release-notes]
+
+::::{NOTE}
+
+::::
+
+
+{{#prs.enhancements}}
+{{#prs.features}}
+### Features and enhancements [${name}-{{versionWithoutPeriods}}-features-enhancements]
+{{{prs.features}}}
+{{/prs.features}}
+{{{prs.enhancements}}}
+{{/prs.enhancements}}
+
+
+{{#prs.fixes}}
+### Fixes [${name}-{{versionWithoutPeriods}}-fixes]
+{{{prs.fixes}}}
+{{/prs.fixes}}`;
+
+  return {
+    pages: {
+      releaseNotes:
+        patchTemplate +
+        `
+
+{{#prs.breaking}}
+---
+navigation_title: "Breaking changes"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/breaking-changes-summary.html
+---
+# ${escapedTemplateNameTag} breaking changes [${name}-breaking-changes]
+Breaking changes can impact your Elastic applications, potentially disrupting normal operations. Before you upgrade, carefully review the ${escapedTemplateNameTag} breaking changes and take the necessary steps to mitigate any issues. To learn how to upgrade, check [Upgrade](/deploy-manage/upgrade.md).
path: src/config/templates/common.ts
user: login: Ikuni17
body: > Thanks, I also had this (started with Kibana) but yours is probably more robust :D https://github.com/elastic/kibana-release-notes/pull/43
>
> I've been using this trimmed down version for the past 2 releases (for Kibana) and it's fine if you want to take a look

Thanks for the review @florent-leborgne, I will bring your template changes into this PR, then we can close #43. This PR will still handle ASCIIDOC for `8.19` releases as well.

> Since we're always going to be pasting into the same files cumulatively, no need to regenerate the start of the files each time. IMO

To be clear, is this only for breaking changes and deprecations or for the main notes generation as well?

> The only thing that's not ideal in my template above is that enhancements are after features instead of grouped together properly, in case you have ideas around making this a unified list :D

My template is the same as yours, listing all features, then all enhancements. What is the proper way they are supposed to be grouped? Also, I see in your template that there are only enhancements and no features in patch releases. Is this always the case? It makes sense, but I feel like we should include the feature PR just in case.
created_at: 2025-05-06T19:31:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,381 @@
+import { AreaDefinition } from '.';
+import type { OutputTemplate, OutputTemplateOptions } from './types';
+
+const createEscapedTag = (str: string) => `{{=<% %>=}}{{${str}}}<%={{ }}=%>`;
+export const kibPullTag = createEscapedTag('kib-pull');
+
+export const generateMarkdownTemplate = ({
+  name,
+  navigationTitle,
+  templateNameTag = name,
+  urlPath = name,
+}: OutputTemplateOptions): OutputTemplate => {
+  // We want this to render as {{kib}} etc in the MD file, so we change the tag delimiters temporarily
+  const escapedTemplateNameTag = createEscapedTag(templateNameTag);
+
+  const patchTemplate = `---
+navigation_title: "${navigationTitle}"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/release-notes.html
+  - https://www.elastic.co/guide/en/${urlPath}/current/whats-new.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/release-notes-{{version}}.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/enhancements-and-bug-fixes-v{{version}}.html
+---
+
+# ${escapedTemplateNameTag} release notes [${name}-release-notes]
+
+Review the changes, fixes, and more in each version of ${escapedTemplateNameTag}.
+
+To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
+
+% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
+
+% ## version.next [${name}-next-release-notes]
+
+% ### Features and enhancements [${name}-next-features-enhancements]
+% *
+
+% ### Fixes [${name}-next-fixes]
+% *
+
+## {{version}} [${name}-{{versionWithoutPeriods}}-release-notes]
path: src/config/templates/common.ts
user: login: Ikuni17
body: 180f5681deedc0ded73df4ced343446487d5450f
created_at: 2025-05-06T19:32:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,6 +25,6 @@
     "src"
   ],
   "exclude": [
-    "node_modules/@elastic/eui/src"
+    "node_modules/@elastic/eui/src",
path: tsconfig.json
user: login: Ikuni17
body: Pushed cb46b97d027b3f420613b3bedafbe265b5adda9f to revert to `main`
created_at: 2025-05-06T19:33:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,381 @@
+import { AreaDefinition } from '.';
+import type { OutputTemplate, OutputTemplateOptions } from './types';
+
+const createEscapedTag = (str: string) => `{{=<% %>=}}{{${str}}}<%={{ }}=%>`;
+export const kibPullTag = createEscapedTag('kib-pull');
+
+export const generateMarkdownTemplate = ({
+  name,
+  navigationTitle,
+  templateNameTag = name,
+  urlPath = name,
+}: OutputTemplateOptions): OutputTemplate => {
+  // We want this to render as {{kib}} etc in the MD file, so we change the tag delimiters temporarily
+  const escapedTemplateNameTag = createEscapedTag(templateNameTag);
+
+  const patchTemplate = `---
+navigation_title: "${navigationTitle}"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/release-notes.html
+  - https://www.elastic.co/guide/en/${urlPath}/current/whats-new.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/release-notes-{{version}}.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/enhancements-and-bug-fixes-v{{version}}.html
+---
+
+# ${escapedTemplateNameTag} release notes [${name}-release-notes]
+
+Review the changes, fixes, and more in each version of ${escapedTemplateNameTag}.
+
+To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
+
+% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
+
+% ## version.next [${name}-next-release-notes]
+
+% ### Features and enhancements [${name}-next-features-enhancements]
+% *
+
+% ### Fixes [${name}-next-fixes]
+% *
+
+## {{version}} [${name}-{{versionWithoutPeriods}}-release-notes]
+
+::::{NOTE}
+
+::::
+
+
+{{#prs.enhancements}}
+{{#prs.features}}
+### Features and enhancements [${name}-{{versionWithoutPeriods}}-features-enhancements]
+{{{prs.features}}}
+{{/prs.features}}
+{{{prs.enhancements}}}
+{{/prs.enhancements}}
+
+
+{{#prs.fixes}}
+### Fixes [${name}-{{versionWithoutPeriods}}-fixes]
+{{{prs.fixes}}}
+{{/prs.fixes}}`;
+
+  return {
+    pages: {
+      releaseNotes:
+        patchTemplate +
+        `
+
+{{#prs.breaking}}
+---
+navigation_title: "Breaking changes"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/breaking-changes-summary.html
+---
+# ${escapedTemplateNameTag} breaking changes [${name}-breaking-changes]
+Breaking changes can impact your Elastic applications, potentially disrupting normal operations. Before you upgrade, carefully review the ${escapedTemplateNameTag} breaking changes and take the necessary steps to mitigate any issues. To learn how to upgrade, check [Upgrade](/deploy-manage/upgrade.md).
path: src/config/templates/common.ts
user: login: Ikuni17
body: Pushed 0d9f3457ee449ee98f54b25afcc4e6ca17ba81ec and 11992bc58d1a71ae580f935032e74464d7244f0e to bring in your changes from #43
created_at: 2025-05-06T20:55:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,381 @@
+import { AreaDefinition } from '.';
+import type { OutputTemplate, OutputTemplateOptions } from './types';
+
+const createEscapedTag = (str: string) => `{{=<% %>=}}{{${str}}}<%={{ }}=%>`;
+export const kibPullTag = createEscapedTag('kib-pull');
+
+export const generateMarkdownTemplate = ({
+  name,
+  navigationTitle,
+  templateNameTag = name,
+  urlPath = name,
+}: OutputTemplateOptions): OutputTemplate => {
+  // We want this to render as {{kib}} etc in the MD file, so we change the tag delimiters temporarily
+  const escapedTemplateNameTag = createEscapedTag(templateNameTag);
+
+  const patchTemplate = `---
+navigation_title: "${navigationTitle}"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/release-notes.html
+  - https://www.elastic.co/guide/en/${urlPath}/current/whats-new.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/release-notes-{{version}}.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/enhancements-and-bug-fixes-v{{version}}.html
+---
+
+# ${escapedTemplateNameTag} release notes [${name}-release-notes]
+
+Review the changes, fixes, and more in each version of ${escapedTemplateNameTag}.
+
+To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
+
+% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
+
+% ## version.next [${name}-next-release-notes]
+
+% ### Features and enhancements [${name}-next-features-enhancements]
+% *
+
+% ### Fixes [${name}-next-fixes]
+% *
+
+## {{version}} [${name}-{{versionWithoutPeriods}}-release-notes]
+
+::::{NOTE}
+
+::::
+
+
+{{#prs.enhancements}}
+{{#prs.features}}
+### Features and enhancements [${name}-{{versionWithoutPeriods}}-features-enhancements]
+{{{prs.features}}}
+{{/prs.features}}
+{{{prs.enhancements}}}
+{{/prs.enhancements}}
+
+
+{{#prs.fixes}}
+### Fixes [${name}-{{versionWithoutPeriods}}-fixes]
+{{{prs.fixes}}}
+{{/prs.fixes}}`;
+
+  return {
+    pages: {
+      releaseNotes:
+        patchTemplate +
+        `
+
+{{#prs.breaking}}
+---
+navigation_title: "Breaking changes"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/breaking-changes-summary.html
+---
+# ${escapedTemplateNameTag} breaking changes [${name}-breaking-changes]
+Breaking changes can impact your Elastic applications, potentially disrupting normal operations. Before you upgrade, carefully review the ${escapedTemplateNameTag} breaking changes and take the necessary steps to mitigate any issues. To learn how to upgrade, check [Upgrade](/deploy-manage/upgrade.md).
path: src/config/templates/common.ts
user: login: Ikuni17
body: I also pushed 0dfdccec10d2c412133219fde6f5cbb4e542729c since your template doesn't have the start of the file and I assume that is correct. Addresses:

> To be clear, is this only for breaking changes and deprecations or for the main notes generation as well?

We can easily revert this commit my assumption was incorrect.
created_at: 2025-05-06T21:08:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,381 @@
+import { AreaDefinition } from '.';
+import type { OutputTemplate, OutputTemplateOptions } from './types';
+
+const createEscapedTag = (str: string) => `{{=<% %>=}}{{${str}}}<%={{ }}=%>`;
+export const kibPullTag = createEscapedTag('kib-pull');
+
+export const generateMarkdownTemplate = ({
+  name,
+  navigationTitle,
+  templateNameTag = name,
+  urlPath = name,
+}: OutputTemplateOptions): OutputTemplate => {
+  // We want this to render as {{kib}} etc in the MD file, so we change the tag delimiters temporarily
+  const escapedTemplateNameTag = createEscapedTag(templateNameTag);
+
+  const patchTemplate = `---
+navigation_title: "${navigationTitle}"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/release-notes.html
+  - https://www.elastic.co/guide/en/${urlPath}/current/whats-new.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/release-notes-{{version}}.html
+  - https://www.elastic.co/guide/en/${urlPath}/master/enhancements-and-bug-fixes-v{{version}}.html
+---
+
+# ${escapedTemplateNameTag} release notes [${name}-release-notes]
+
+Review the changes, fixes, and more in each version of ${escapedTemplateNameTag}.
+
+To check for security updates, go to [Security announcements for the Elastic stack](https://discuss.elastic.co/c/announcements/security-announcements/31).
+
+% Release notes include only features, enhancements, and fixes. Add breaking changes, deprecations, and known issues to the applicable release notes sections.
+
+% ## version.next [${name}-next-release-notes]
+
+% ### Features and enhancements [${name}-next-features-enhancements]
+% *
+
+% ### Fixes [${name}-next-fixes]
+% *
+
+## {{version}} [${name}-{{versionWithoutPeriods}}-release-notes]
+
+::::{NOTE}
+
+::::
+
+
+{{#prs.enhancements}}
+{{#prs.features}}
+### Features and enhancements [${name}-{{versionWithoutPeriods}}-features-enhancements]
+{{{prs.features}}}
+{{/prs.features}}
+{{{prs.enhancements}}}
+{{/prs.enhancements}}
+
+
+{{#prs.fixes}}
+### Fixes [${name}-{{versionWithoutPeriods}}-fixes]
+{{{prs.fixes}}}
+{{/prs.fixes}}`;
+
+  return {
+    pages: {
+      releaseNotes:
+        patchTemplate +
+        `
+
+{{#prs.breaking}}
+---
+navigation_title: "Breaking changes"
+mapped_pages:
+  - https://www.elastic.co/guide/en/${urlPath}/current/breaking-changes-summary.html
+---
+# ${escapedTemplateNameTag} breaking changes [${name}-breaking-changes]
+Breaking changes can impact your Elastic applications, potentially disrupting normal operations. Before you upgrade, carefully review the ${escapedTemplateNameTag} breaking changes and take the necessary steps to mitigate any issues. To learn how to upgrade, check [Upgrade](/deploy-manage/upgrade.md).
path: src/config/templates/common.ts
user: login: florent-leborgne
body: Hey @Ikuni17 thank you so much for your help with this.

> To be clear, is this only for breaking changes and deprecations or for the main notes generation as well?

It's for all files, correct. Thank you!

> What is the proper way they are supposed to be grouped?

Right I saw that we were doing the same thing actually after my comment. Previously (in asciidoc) we had:

```
=== Features
* Discover feature 1
* Discover feature 2
* Management feature 1
* Management feature 2

=== Enhancements
* Discover enhancement 1
* Discover enhancement 2

etc.
```

Now in Markdown, in addition to the format change and as you noticed, we want to have:

```
### Features and enhancements
* Discover feature 1
* Discover feature 2
* Discover enhancement 1
* Discover enhancement 2
* Management feature 1
* Management feature 2

etc.
```

Meaning that in what we deliver at then end, it makes more sense to have features and enhancement of the same category to be together, instead of having all features, then all enhancements. (Do I make sense? 😅). I was not sure how to resolve this with my limited skills, so I initially just put them one after the other and figured we'd edit the ordering manually. But just in case you can think of something... No worries if that makes it all too complex as well.

> Also, I see in your template that there are only enhancements and no features in patch releases. Is this always the case? It makes sense, but I feel like we should include the feature PR just in case.

I think you are correct here. I basically just edited the asciidoc template into markdown to reproduce the same, but I may have misread the patch one. If there can be features in patches, it's best if we can prepare the template for that possibility. Thanks!
created_at: 2025-05-07T07:21:21Z
author_association: MEMBER

PR comments:
user: login: florent-leborgne
created_at: 2025-05-06T15:28:47Z
body: Thanks, I also had this (started with Kibana) but yours is probably more robust :D https://github.com/elastic/kibana-release-notes/pull/43 
author_association: MEMBER
user: login: Ikuni17
created_at: 2025-05-06T21:30:40Z
body: @delanni I pushed da981bf9eac6c99a7cb4c4cfb331856f54a74f14 to hopefully address the readability. Let me know if you have other ideas.

I have also updated the examples after this round of feedback. (cc @florent-leborgne)
author_association: CONTRIBUTOR
user: login: florent-leborgne
created_at: 2025-05-07T16:29:34Z
body: I still want to review this but haven't had the time yet :) Just looking at the output I think there are a few tweaks to make to the serverless one especially about this extract:

In serverless release notes Security and Observability changes should be included (we don't have a specific page for security serverless or observability serverless)

```
For the Elastic Observability 04282025 release information, refer to [Elastic Observability Solution Release Notes](docs-content://release-notes/elastic-observability/index.md).
For the Elastic Security 04282025 release information, refer to [Elastic Security Solution Release Notes](docs-content://release-notes/elastic-security/index.md).
For the Elastic Observability 04282025 release information, refer to [Elastic Observability Solution Release Notes](docs-content://release-notes/elastic-observability/index.md).
For the Elastic Security 04282025 release information, refer to [Elastic Security Solution Release Notes](docs-content://release-notes/elastic-security/index.md).
```
author_association: MEMBER
user: login: Ikuni17
created_at: 2025-05-08T00:57:08Z
body: @florent-leborgne If there are any small template changes remaining feel free to push to this branch since you will know better than me. The examples have been updated again to reflect new changes. I fixed the grouping of features and enhancements with 2231c6540ee5a66e1d4f84244044517ace72fdc2, this applies to both normal and Serverless release notes.

> In serverless release notes Security and Observability changes should be included (we don't have a specific page for security serverless or observability serverless)

Fixed with 2f85d25d4156efbb36f130a40e658d9c34f8f86c.



author_association: CONTRIBUTOR