From 97ea44bbbfe114e659d77cdb60575d1e6a9aa880 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 13 May 2026 13:45:26 +0200 Subject: [PATCH 1/9] feat: Add snippets from all code tabs to copied markdown --- scripts/generate-md-exports.mjs | 67 +++++++++ scripts/generate-md-exports.test.mjs | 199 +++++++++++++++++++++++++++ src/components/codeTabs.tsx | 12 +- 3 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-md-exports.test.mjs diff --git a/scripts/generate-md-exports.mjs b/scripts/generate-md-exports.mjs index b4dd6007b8de62..2ad5eae117cd5a 100644 --- a/scripts/generate-md-exports.mjs +++ b/scripts/generate-md-exports.mjs @@ -26,6 +26,7 @@ import RemarkLinkRewrite from 'remark-link-rewrite'; import remarkStringify from 'remark-stringify'; import {unified} from 'unified'; import {remove} from 'unist-util-remove'; +import {visit} from 'unist-util-visit'; const DOCS_ORIGIN = process.env.NEXT_PUBLIC_DEVELOPER_DOCS ? 'https://develop.sentry.dev' @@ -957,6 +958,71 @@ function extractContentForCacheKey(html) { return title + '\0' + canonical + '\0' + normalizedMain; } +/** + * Rehype plugin that expands CodeTabs panels for markdown export. + * + * CodeTabs renders all tab panels in the DOM but hides non-active ones with + * the `hidden` attribute. Each panel carries `data-code-tab-title` and + * optionally `data-code-tab-filename`. This plugin: + * 1. Removes the `hidden` attribute so rehypeRemark processes every panel + * 2. Strips CodeBlock chrome (filename display, copy button) from the panel + * 3. Inserts a bold heading (preferring filename, falling back to tab title) + * before the
block so readers can tell which file the snippet
+ * belongs to
+ */
+export function rehypeExpandCodeTabs() {
+ return tree => {
+ visit(tree, 'element', node => {
+ const title = node.properties?.dataCodeTabTitle;
+ if (!title) {
+ return;
+ }
+ delete node.properties.hidden;
+ const filename = node.properties?.dataCodeTabFilename;
+ delete node.properties.dataCodeTabTitle;
+ delete node.properties.dataCodeTabFilename;
+
+ const label = filename || title;
+ const preElements = collectAll(node, el => el.tagName === 'pre');
+ if (preElements.length === 0) {
+ return;
+ }
+
+ const heading = {
+ type: 'element',
+ tagName: 'p',
+ properties: {},
+ children: [
+ {
+ type: 'element',
+ tagName: 'strong',
+ properties: {},
+ children: [{type: 'text', value: label}],
+ },
+ ],
+ };
+
+ node.children = [heading, ...preElements];
+ });
+ };
+}
+
+function collectAll(node, predicate) {
+ const results = [];
+ (function walk(n) {
+ if (n.type === 'element' && predicate(n)) {
+ results.push(n);
+ return;
+ }
+ if (n.children) {
+ for (const child of n.children) {
+ walk(child);
+ }
+ }
+ })(node);
+ return results;
+}
+
async function genMDFromHTML(source, {cacheDir, noCache, usedCacheFiles}) {
const rawHTML = await readFile(source, {encoding: 'utf8'});
// Strip build-specific HTML elements for faster parsing.
@@ -1004,6 +1070,7 @@ async function genMDFromHTML(source, {cacheDir, noCache, usedCacheFiles}) {
properties: {},
children: tree,
}))
+ .use(rehypeExpandCodeTabs)
.use(rehypeRemark, {
document: false,
handlers: {
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
new file mode 100644
index 00000000000000..9bce820ae703fc
--- /dev/null
+++ b/scripts/generate-md-exports.test.mjs
@@ -0,0 +1,199 @@
+import rehypeParse from 'rehype-parse';
+import rehypeRemark from 'rehype-remark';
+import remarkGfm from 'remark-gfm';
+import remarkStringify from 'remark-stringify';
+import {describe, expect, it} from 'vitest';
+import {unified} from 'unified';
+import {remove} from 'unist-util-remove';
+
+import {rehypeExpandCodeTabs} from './generate-md-exports.mjs';
+
+function htmlToMarkdown(html) {
+ return String(
+ unified()
+ .use(rehypeParse)
+ .use(rehypeExpandCodeTabs)
+ .use(rehypeRemark, {
+ document: false,
+ handlers: {
+ button() {},
+ },
+ })
+ .use(() => tree => remove(tree, {type: 'inlineCode', value: ''}))
+ .use(remarkGfm)
+ .use(remarkStringify)
+ .processSync(html)
+ );
+}
+
+/**
+ * Builds a minimal CodeTabs HTML structure matching what the component renders.
+ * Each tab panel contains the CodeBlock chrome (filename display, copy button)
+ * wrapping the actual block. The first tab is visible; subsequent
+ * tabs have the `hidden` attribute.
+ */
+function buildCodeTabsHTML(tabs) {
+ const buttons = tabs
+ .map((t, i) => ``)
+ .join('');
+
+ const panels = tabs
+ .map((t, i) => {
+ const filenameAttr = t.filename
+ ? ` data-code-tab-filename="${t.filename}"`
+ : '';
+ const hiddenAttr = i > 0 ? ' hidden' : '';
+
+ return (
+ `` +
+ `` +
+ `` +
+ `${t.filename || ''}` +
+ `` +
+ `` +
+ `Copied` +
+ `${t.code}
` +
+ `` +
+ ``
+ );
+ })
+ .join('');
+
+ return `${buttons}${panels}`;
+}
+
+describe('rehypeExpandCodeTabs', () => {
+ it('expands hidden tabs and labels each with a bold heading', () => {
+ const html = buildCodeTabsHTML([
+ {
+ title: 'Cloudflare Workers',
+ lang: 'javascript',
+ code: 'import { sentry } from "@sentry/hono/cloudflare";',
+ },
+ {
+ title: 'Node.js',
+ filename: 'instrument.mjs',
+ lang: 'javascript',
+ code: 'import * as Sentry from "@sentry/hono/node";',
+ },
+ {
+ title: 'Bun',
+ lang: 'javascript',
+ code: 'import { sentry } from "@sentry/hono/bun";',
+ },
+ ]);
+
+ const md = htmlToMarkdown(html);
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+
+ expect(codeBlocks).toHaveLength(3);
+ expect(md).toContain('**Cloudflare Workers**');
+ expect(md).toContain('**instrument.mjs**');
+ expect(md).toContain('**Bun**');
+ expect(codeBlocks[0]).toContain('@sentry/hono/cloudflare');
+ expect(codeBlocks[1]).toContain('@sentry/hono/node');
+ expect(codeBlocks[2]).toContain('@sentry/hono/bun');
+ });
+
+ it('prefers filename over tab title for the heading', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'},
+ {title: 'CommonJS', filename: 'instrument.js', lang: 'javascript', code: 'init();'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ expect(md).toContain('**instrument.mjs**');
+ expect(md).toContain('**instrument.js**');
+ expect(md).not.toContain('**ESM**');
+ expect(md).not.toContain('**CommonJS**');
+ });
+
+ it('falls back to tab title when no filename is set', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'Cloudflare Workers', lang: 'javascript', code: 'workers();'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ expect(md).toContain('**Cloudflare Workers**');
+ });
+
+ it('strips CodeBlock chrome (filename display, copy button, copied indicator)', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ expect(md).not.toContain('Copied');
+ expect(md).not.toContain('`instrument.mjs`');
+ });
+
+ it('does not affect code blocks outside of tabs', () => {
+ const html =
+ 'curl -sL https://sentry.io/get-cli/ | bash
';
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(1);
+ expect(md).not.toMatch(/\*\*.*\*\*\n/);
+ expect(codeBlocks[0]).toContain('curl -sL');
+ });
+
+ it('keeps standalone blocks intact when mixed with tabs on the same page', () => {
+ const standalone =
+ 'npm install @sentry/node
';
+ const tabs = buildCodeTabsHTML([
+ {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'},
+ {title: 'Bun', lang: 'javascript', code: 'init();'},
+ ]);
+ const html = `${standalone}${tabs}`;
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(3);
+ expect(codeBlocks[0]).toContain('npm install');
+ expect(md).toContain('**instrument.mjs**');
+ expect(md).toContain('**Bun**');
+ });
+
+ it('handles two separate tab groups on the same page', () => {
+ const group1 = buildCodeTabsHTML([
+ {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'},
+ {title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'},
+ ]);
+ const group2 = buildCodeTabsHTML([
+ {title: 'Python', filename: 'main.py', lang: 'python', code: 'import sentry_sdk'},
+ {title: 'Ruby', filename: 'config.rb', lang: 'ruby', code: 'require "sentry-ruby"'},
+ ]);
+ const html = `${group1}${group2}`;
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(4);
+ expect(md).toContain('**instrument.mjs**');
+ expect(md).toContain('**instrument.js**');
+ expect(md).toContain('**main.py**');
+ expect(md).toContain('**config.rb**');
+ });
+
+ it('handles tab panel with no pre element gracefully', () => {
+ const html =
+ '' +
+ 'Not a code block
' +
+ '' +
+ 'works();
' +
+ '' +
+ '';
+
+ const md = htmlToMarkdown(html);
+
+ expect(md).toContain('**ok**');
+ expect(md).toContain('works()');
+ expect(md).not.toContain('**broken**');
+ });
+});
diff --git a/src/components/codeTabs.tsx b/src/components/codeTabs.tsx
index bfe1a22be428b6..fa4d8784218631 100644
--- a/src/components/codeTabs.tsx
+++ b/src/components/codeTabs.tsx
@@ -157,7 +157,17 @@ export function CodeTabs({children}: CodeTabProps) {
{showSigninNote(codeBlocks[selectedTabIndex]) && }
{buttons}
- {codeBlocks[selectedTabIndex]}
+ {codeBlocks.map((block, idx) => (
+
+ {block}
+
+ ))}
);
}
From 082c3ea743b111d053651247e20bfee1df3cef78 Mon Sep 17 00:00:00 2001
From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 13:16:50 +0000
Subject: [PATCH 2/9] [getsentry/action-github-commit] Auto commit
---
scripts/generate-md-exports.test.mjs | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index 9bce820ae703fc..e5620a79bca0aa 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -39,9 +39,7 @@ function buildCodeTabsHTML(tabs) {
const panels = tabs
.map((t, i) => {
- const filenameAttr = t.filename
- ? ` data-code-tab-filename="${t.filename}"`
- : '';
+ const filenameAttr = t.filename ? ` data-code-tab-filename="${t.filename}"` : '';
const hiddenAttr = i > 0 ? ' hidden' : '';
return (
@@ -146,7 +144,12 @@ describe('rehypeExpandCodeTabs', () => {
const standalone =
'npm install @sentry/node
';
const tabs = buildCodeTabsHTML([
- {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'},
+ {
+ title: 'Node.js',
+ filename: 'instrument.mjs',
+ lang: 'javascript',
+ code: 'Sentry.init();',
+ },
{title: 'Bun', lang: 'javascript', code: 'init();'},
]);
const html = `${standalone}${tabs}`;
From 56c2da52d75d5922e803e1463edba68ffe7c84f7 Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Wed, 13 May 2026 15:34:34 +0200
Subject: [PATCH 3/9] externalize plugin
---
scripts/generate-md-exports.mjs | 68 +---------------------------
scripts/generate-md-exports.test.mjs | 2 +-
scripts/rehype-expand-code-tabs.mjs | 66 +++++++++++++++++++++++++++
3 files changed, 69 insertions(+), 67 deletions(-)
create mode 100644 scripts/rehype-expand-code-tabs.mjs
diff --git a/scripts/generate-md-exports.mjs b/scripts/generate-md-exports.mjs
index 2ad5eae117cd5a..95c5a67e360f16 100644
--- a/scripts/generate-md-exports.mjs
+++ b/scripts/generate-md-exports.mjs
@@ -26,7 +26,8 @@ import RemarkLinkRewrite from 'remark-link-rewrite';
import remarkStringify from 'remark-stringify';
import {unified} from 'unified';
import {remove} from 'unist-util-remove';
-import {visit} from 'unist-util-visit';
+
+import {rehypeExpandCodeTabs} from './rehype-expand-code-tabs.mjs';
const DOCS_ORIGIN = process.env.NEXT_PUBLIC_DEVELOPER_DOCS
? 'https://develop.sentry.dev'
@@ -958,71 +959,6 @@ function extractContentForCacheKey(html) {
return title + '\0' + canonical + '\0' + normalizedMain;
}
-/**
- * Rehype plugin that expands CodeTabs panels for markdown export.
- *
- * CodeTabs renders all tab panels in the DOM but hides non-active ones with
- * the `hidden` attribute. Each panel carries `data-code-tab-title` and
- * optionally `data-code-tab-filename`. This plugin:
- * 1. Removes the `hidden` attribute so rehypeRemark processes every panel
- * 2. Strips CodeBlock chrome (filename display, copy button) from the panel
- * 3. Inserts a bold heading (preferring filename, falling back to tab title)
- * before the block so readers can tell which file the snippet
- * belongs to
- */
-export function rehypeExpandCodeTabs() {
- return tree => {
- visit(tree, 'element', node => {
- const title = node.properties?.dataCodeTabTitle;
- if (!title) {
- return;
- }
- delete node.properties.hidden;
- const filename = node.properties?.dataCodeTabFilename;
- delete node.properties.dataCodeTabTitle;
- delete node.properties.dataCodeTabFilename;
-
- const label = filename || title;
- const preElements = collectAll(node, el => el.tagName === 'pre');
- if (preElements.length === 0) {
- return;
- }
-
- const heading = {
- type: 'element',
- tagName: 'p',
- properties: {},
- children: [
- {
- type: 'element',
- tagName: 'strong',
- properties: {},
- children: [{type: 'text', value: label}],
- },
- ],
- };
-
- node.children = [heading, ...preElements];
- });
- };
-}
-
-function collectAll(node, predicate) {
- const results = [];
- (function walk(n) {
- if (n.type === 'element' && predicate(n)) {
- results.push(n);
- return;
- }
- if (n.children) {
- for (const child of n.children) {
- walk(child);
- }
- }
- })(node);
- return results;
-}
-
async function genMDFromHTML(source, {cacheDir, noCache, usedCacheFiles}) {
const rawHTML = await readFile(source, {encoding: 'utf8'});
// Strip build-specific HTML elements for faster parsing.
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index e5620a79bca0aa..b51f1bd90aea5a 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -6,7 +6,7 @@ import {describe, expect, it} from 'vitest';
import {unified} from 'unified';
import {remove} from 'unist-util-remove';
-import {rehypeExpandCodeTabs} from './generate-md-exports.mjs';
+import {rehypeExpandCodeTabs} from './rehype-expand-code-tabs.mjs';
function htmlToMarkdown(html) {
return String(
diff --git a/scripts/rehype-expand-code-tabs.mjs b/scripts/rehype-expand-code-tabs.mjs
new file mode 100644
index 00000000000000..99a10b09726093
--- /dev/null
+++ b/scripts/rehype-expand-code-tabs.mjs
@@ -0,0 +1,66 @@
+import {visit} from 'unist-util-visit';
+
+/**
+ * Rehype plugin that expands CodeTabs panels for markdown export.
+ *
+ * CodeTabs renders all tab panels in the DOM but hides non-active ones with
+ * the `hidden` attribute. Each panel carries `data-code-tab-title` and
+ * optionally `data-code-tab-filename`. This plugin:
+ * 1. Removes the `hidden` attribute so rehypeRemark processes every panel
+ * 2. Strips CodeBlock chrome (filename display, copy button) from the panel
+ * 3. Inserts a bold heading (preferring filename, falling back to tab title)
+ * before the block so readers can tell which file the snippet
+ * belongs to
+ */
+export function rehypeExpandCodeTabs() {
+ return tree => {
+ visit(tree, 'element', node => {
+ const title = node.properties?.dataCodeTabTitle;
+ if (!title) {
+ return;
+ }
+ delete node.properties.hidden;
+ const filename = node.properties?.dataCodeTabFilename;
+ delete node.properties.dataCodeTabTitle;
+ delete node.properties.dataCodeTabFilename;
+
+ const label = filename || title;
+ const preElements = collectAll(node, el => el.tagName === 'pre');
+ if (preElements.length === 0) {
+ return;
+ }
+
+ const heading = {
+ type: 'element',
+ tagName: 'p',
+ properties: {},
+ children: [
+ {
+ type: 'element',
+ tagName: 'strong',
+ properties: {},
+ children: [{type: 'text', value: label}],
+ },
+ ],
+ };
+
+ node.children = [heading, ...preElements];
+ });
+ };
+}
+
+function collectAll(node, predicate) {
+ const results = [];
+ (function walk(n) {
+ if (n.type === 'element' && predicate(n)) {
+ results.push(n);
+ return;
+ }
+ if (n.children) {
+ for (const child of n.children) {
+ walk(child);
+ }
+ }
+ })(node);
+ return results;
+}
From 2fa66eced61e9a93d3b74dbffd6a2b40d1924550 Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Wed, 13 May 2026 16:22:16 +0200
Subject: [PATCH 4/9] annother approach
---
scripts/generate-md-exports.test.mjs | 130 +++++++++++++++------------
scripts/rehype-expand-code-tabs.mjs | 70 +++++++++------
src/components/codeTabs.tsx | 12 +--
src/remark-code-tabs.js | 19 ++++
4 files changed, 134 insertions(+), 97 deletions(-)
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index b51f1bd90aea5a..d4b25a5feaec0b 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -27,56 +27,74 @@ function htmlToMarkdown(html) {
}
/**
- * Builds a minimal CodeTabs HTML structure matching what the component renders.
- * Each tab panel contains the CodeBlock chrome (filename display, copy button)
- * wrapping the actual block. The first tab is visible; subsequent
- * tabs have the `hidden` attribute.
+ * Builds HTML matching what the remark-code-tabs plugin + CodeTabs component
+ * produce in the static build output:
+ *
+ *
+ *
+ *
+ *
+ *
+ * file1
+ * code1
+ *
+ *
+ *
+ *
+ * code1
+ *
+ *
+ * code2
+ *
+ *
*/
function buildCodeTabsHTML(tabs) {
- const buttons = tabs
- .map((t, i) => ``)
- .join('');
-
- const panels = tabs
- .map((t, i) => {
- const filenameAttr = t.filename ? ` data-code-tab-filename="${t.filename}"` : '';
- const hiddenAttr = i > 0 ? ' hidden' : '';
-
+ const firstTab = tabs[0];
+
+ const codeTabsRendered =
+ '' +
+ tabs.map((t, i) => ``).join('') +
+ '' +
+ `${firstTab.filename || ''}` +
+ `${firstTab.code}
` +
+ '' +
+ '';
+
+ const exportBlocks = tabs
+ .map(t => {
+ const filenameAttr = t.filename
+ ? ` data-code-tab-filename="${t.filename}"`
+ : '';
return (
- `` +
- `` +
- `` +
- `${t.filename || ''}` +
- `` +
- `` +
- `Copied` +
- `${t.code}
` +
- `` +
- ``
+ `` +
+ `${t.code}
` +
+ ''
);
})
.join('');
- return `${buttons}${panels}`;
+ return `${codeTabsRendered}${exportBlocks}`;
}
describe('rehypeExpandCodeTabs', () => {
- it('expands hidden tabs and labels each with a bold heading', () => {
+ it('replaces wrapper content with all tabs, not just the active one', () => {
const html = buildCodeTabsHTML([
{
title: 'Cloudflare Workers',
- lang: 'javascript',
+ filename: 'index.ts',
+ lang: 'typescript',
code: 'import { sentry } from "@sentry/hono/cloudflare";',
},
{
title: 'Node.js',
- filename: 'instrument.mjs',
- lang: 'javascript',
- code: 'import * as Sentry from "@sentry/hono/node";',
+ filename: 'app.ts',
+ lang: 'typescript',
+ code: 'import { sentry } from "@sentry/hono/node";',
},
{
title: 'Bun',
- lang: 'javascript',
+ filename: 'index.ts',
+ lang: 'typescript',
code: 'import { sentry } from "@sentry/hono/bun";',
},
]);
@@ -85,14 +103,26 @@ describe('rehypeExpandCodeTabs', () => {
const codeBlocks = md.match(/```[\s\S]*?```/g);
expect(codeBlocks).toHaveLength(3);
- expect(md).toContain('**Cloudflare Workers**');
- expect(md).toContain('**instrument.mjs**');
- expect(md).toContain('**Bun**');
+ expect(md).toContain('**index.ts**');
+ expect(md).toContain('**app.ts**');
expect(codeBlocks[0]).toContain('@sentry/hono/cloudflare');
expect(codeBlocks[1]).toContain('@sentry/hono/node');
expect(codeBlocks[2]).toContain('@sentry/hono/bun');
});
+ it('removes CodeTabs-rendered content (tab buttons, filename display, active tab)', () => {
+ const html = buildCodeTabsHTML([
+ {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'},
+ {title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'},
+ ]);
+
+ const md = htmlToMarkdown(html);
+
+ expect(md).not.toContain('`instrument.mjs`');
+ expect(md).not.toContain('ESM');
+ expect(md).not.toContain('CJS');
+ });
+
it('prefers filename over tab title for the heading', () => {
const html = buildCodeTabsHTML([
{title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'},
@@ -110,25 +140,16 @@ describe('rehypeExpandCodeTabs', () => {
it('falls back to tab title when no filename is set', () => {
const html = buildCodeTabsHTML([
{title: 'Cloudflare Workers', lang: 'javascript', code: 'workers();'},
+ {title: 'Bun', lang: 'javascript', code: 'bun();'},
]);
const md = htmlToMarkdown(html);
expect(md).toContain('**Cloudflare Workers**');
+ expect(md).toContain('**Bun**');
});
- it('strips CodeBlock chrome (filename display, copy button, copied indicator)', () => {
- const html = buildCodeTabsHTML([
- {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'},
- ]);
-
- const md = htmlToMarkdown(html);
-
- expect(md).not.toContain('Copied');
- expect(md).not.toContain('`instrument.mjs`');
- });
-
- it('does not affect code blocks outside of tabs', () => {
+ it('does not affect code blocks outside of tab wrappers', () => {
const html =
'curl -sL https://sentry.io/get-cli/ | bash
';
@@ -144,12 +165,7 @@ describe('rehypeExpandCodeTabs', () => {
const standalone =
'npm install @sentry/node
';
const tabs = buildCodeTabsHTML([
- {
- title: 'Node.js',
- filename: 'instrument.mjs',
- lang: 'javascript',
- code: 'Sentry.init();',
- },
+ {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'},
{title: 'Bun', lang: 'javascript', code: 'init();'},
]);
const html = `${standalone}${tabs}`;
@@ -184,13 +200,12 @@ describe('rehypeExpandCodeTabs', () => {
expect(md).toContain('**config.rb**');
});
- it('handles tab panel with no pre element gracefully', () => {
+ it('skips export block with no pre element', () => {
const html =
- '' +
- 'Not a code block
' +
- '' +
- 'works();
' +
- '' +
+ '' +
+ 'active tab
' +
+ 'Not a code block
' +
+ 'works();
' +
'';
const md = htmlToMarkdown(html);
@@ -198,5 +213,6 @@ describe('rehypeExpandCodeTabs', () => {
expect(md).toContain('**ok**');
expect(md).toContain('works()');
expect(md).not.toContain('**broken**');
+ expect(md).not.toContain('active tab');
});
});
diff --git a/scripts/rehype-expand-code-tabs.mjs b/scripts/rehype-expand-code-tabs.mjs
index 99a10b09726093..44183cc14d0aba 100644
--- a/scripts/rehype-expand-code-tabs.mjs
+++ b/scripts/rehype-expand-code-tabs.mjs
@@ -1,50 +1,62 @@
import {visit} from 'unist-util-visit';
/**
- * Rehype plugin that expands CodeTabs panels for markdown export.
+ * Rehype plugin that expands CodeTabs for markdown export.
*
- * CodeTabs renders all tab panels in the DOM but hides non-active ones with
- * the `hidden` attribute. Each panel carries `data-code-tab-title` and
- * optionally `data-code-tab-filename`. This plugin:
- * 1. Removes the `hidden` attribute so rehypeRemark processes every panel
- * 2. Strips CodeBlock chrome (filename display, copy button) from the panel
- * 3. Inserts a bold heading (preferring filename, falling back to tab title)
- * before the block so readers can tell which file the snippet
- * belongs to
+ * The remark-code-tabs plugin injects hidden
+ * blocks alongside the interactive component inside each
+ * .code-tabs-wrapper. These hidden blocks contain the raw code for every
+ * tab and are always present in the static HTML (unlike CodeTabs output,
+ * which may only include the active tab due to RSC serialization).
+ *
+ * This plugin:
+ * 1. Finds parent elements that contain [data-code-tab-title] children
+ * 2. Replaces ALL children with expanded export blocks (removing the
+ * CodeTabs-rendered content to avoid duplication)
+ * 3. Each export block becomes a bold heading + fenced code block,
+ * preferring filename over tab title for the heading
*/
export function rehypeExpandCodeTabs() {
return tree => {
visit(tree, 'element', node => {
- const title = node.properties?.dataCodeTabTitle;
- if (!title) {
+ if (!node.children) {
return;
}
- delete node.properties.hidden;
- const filename = node.properties?.dataCodeTabFilename;
- delete node.properties.dataCodeTabTitle;
- delete node.properties.dataCodeTabFilename;
- const label = filename || title;
- const preElements = collectAll(node, el => el.tagName === 'pre');
- if (preElements.length === 0) {
+ const exportBlocks = node.children.filter(
+ child => child.type === 'element' && child.properties?.dataCodeTabTitle
+ );
+ if (exportBlocks.length === 0) {
return;
}
- const heading = {
- type: 'element',
- tagName: 'p',
- properties: {},
- children: [
+ node.children = exportBlocks.flatMap(block => {
+ const title = block.properties.dataCodeTabTitle;
+ const filename = block.properties.dataCodeTabFilename;
+ const label = filename || title;
+
+ const preElements = collectAll(block, el => el.tagName === 'pre');
+ if (preElements.length === 0) {
+ return [];
+ }
+
+ return [
{
type: 'element',
- tagName: 'strong',
+ tagName: 'p',
properties: {},
- children: [{type: 'text', value: label}],
+ children: [
+ {
+ type: 'element',
+ tagName: 'strong',
+ properties: {},
+ children: [{type: 'text', value: label}],
+ },
+ ],
},
- ],
- };
-
- node.children = [heading, ...preElements];
+ ...preElements,
+ ];
+ });
});
};
}
diff --git a/src/components/codeTabs.tsx b/src/components/codeTabs.tsx
index fa4d8784218631..bfe1a22be428b6 100644
--- a/src/components/codeTabs.tsx
+++ b/src/components/codeTabs.tsx
@@ -157,17 +157,7 @@ export function CodeTabs({children}: CodeTabProps) {
{showSigninNote(codeBlocks[selectedTabIndex]) && }
{buttons}
- {codeBlocks.map((block, idx) => (
-
- {block}
-
- ))}
+ {codeBlocks[selectedTabIndex]}
);
}
diff --git a/src/remark-code-tabs.js b/src/remark-code-tabs.js
index c613776cb27ac8..2f1bdb654accab 100644
--- a/src/remark-code-tabs.js
+++ b/src/remark-code-tabs.js
@@ -66,6 +66,24 @@ export default function remarkCodeTabs() {
[]
);
+ const exportBlocks = pendingCode.map(([node]) => {
+ const title = getTabTitle(node);
+ const filename = getFilename(node);
+ const lang = fixLanguage(node);
+ const hProperties = {
+ hidden: true,
+ dataCodeTabTitle: title || lang,
+ };
+ if (filename) {
+ hProperties.dataCodeTabFilename = filename;
+ }
+ return {
+ type: 'element',
+ data: {hName: 'div', hProperties},
+ children: [{type: 'code', lang, value: node.value}],
+ };
+ });
+
rootNode.type = 'element';
rootNode.data = {
hName: 'div',
@@ -79,6 +97,7 @@ export default function remarkCodeTabs() {
name: 'CodeTabs',
children,
},
+ ...exportBlocks,
];
toRemove = toRemove.concat(pendingCode.splice(1));
From 45b839ac185818f751c44a426fd758ffd3e7c5da Mon Sep 17 00:00:00 2001
From: "getsantry[bot]" <66042841+getsantry[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 14:23:29 +0000
Subject: [PATCH 5/9] [getsentry/action-github-commit] Auto commit
---
scripts/generate-md-exports.test.mjs | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index d4b25a5feaec0b..722118a0b6ddc2 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -62,9 +62,7 @@ function buildCodeTabsHTML(tabs) {
const exportBlocks = tabs
.map(t => {
- const filenameAttr = t.filename
- ? ` data-code-tab-filename="${t.filename}"`
- : '';
+ const filenameAttr = t.filename ? ` data-code-tab-filename="${t.filename}"` : '';
return (
`` +
`${t.code}
` +
@@ -165,7 +163,12 @@ describe('rehypeExpandCodeTabs', () => {
const standalone =
'npm install @sentry/node
';
const tabs = buildCodeTabsHTML([
- {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'},
+ {
+ title: 'Node.js',
+ filename: 'instrument.mjs',
+ lang: 'javascript',
+ code: 'Sentry.init();',
+ },
{title: 'Bun', lang: 'javascript', code: 'init();'},
]);
const html = `${standalone}${tabs}`;
From c5045ace39f443840186e46407cd8c6e5a62eb2a Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Wed, 13 May 2026 16:50:31 +0200
Subject: [PATCH 6/9] add tab-title
---
scripts/generate-md-exports.test.mjs | 25 +++++++++++--------------
scripts/rehype-expand-code-tabs.mjs | 8 +++++---
2 files changed, 16 insertions(+), 17 deletions(-)
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index 722118a0b6ddc2..3590570f9455cc 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -101,8 +101,9 @@ describe('rehypeExpandCodeTabs', () => {
const codeBlocks = md.match(/```[\s\S]*?```/g);
expect(codeBlocks).toHaveLength(3);
- expect(md).toContain('**index.ts**');
- expect(md).toContain('**app.ts**');
+ expect(md).toContain('**\\[Cloudflare Workers] index.ts**');
+ expect(md).toContain('**\\[Node.js] app.ts**');
+ expect(md).toContain('**\\[Bun] index.ts**');
expect(codeBlocks[0]).toContain('@sentry/hono/cloudflare');
expect(codeBlocks[1]).toContain('@sentry/hono/node');
expect(codeBlocks[2]).toContain('@sentry/hono/bun');
@@ -117,11 +118,9 @@ describe('rehypeExpandCodeTabs', () => {
const md = htmlToMarkdown(html);
expect(md).not.toContain('`instrument.mjs`');
- expect(md).not.toContain('ESM');
- expect(md).not.toContain('CJS');
});
- it('prefers filename over tab title for the heading', () => {
+ it('includes both tab title and filename in heading when both exist', () => {
const html = buildCodeTabsHTML([
{title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'},
{title: 'CommonJS', filename: 'instrument.js', lang: 'javascript', code: 'init();'},
@@ -129,10 +128,8 @@ describe('rehypeExpandCodeTabs', () => {
const md = htmlToMarkdown(html);
- expect(md).toContain('**instrument.mjs**');
- expect(md).toContain('**instrument.js**');
- expect(md).not.toContain('**ESM**');
- expect(md).not.toContain('**CommonJS**');
+ expect(md).toContain('**\\[ESM] instrument.mjs**');
+ expect(md).toContain('**\\[CommonJS] instrument.js**');
});
it('falls back to tab title when no filename is set', () => {
@@ -178,7 +175,7 @@ describe('rehypeExpandCodeTabs', () => {
const codeBlocks = md.match(/```[\s\S]*?```/g);
expect(codeBlocks).toHaveLength(3);
expect(codeBlocks[0]).toContain('npm install');
- expect(md).toContain('**instrument.mjs**');
+ expect(md).toContain('**\\[Node.js] instrument.mjs**');
expect(md).toContain('**Bun**');
});
@@ -197,10 +194,10 @@ describe('rehypeExpandCodeTabs', () => {
const codeBlocks = md.match(/```[\s\S]*?```/g);
expect(codeBlocks).toHaveLength(4);
- expect(md).toContain('**instrument.mjs**');
- expect(md).toContain('**instrument.js**');
- expect(md).toContain('**main.py**');
- expect(md).toContain('**config.rb**');
+ expect(md).toContain('**\\[ESM] instrument.mjs**');
+ expect(md).toContain('**\\[CJS] instrument.js**');
+ expect(md).toContain('**\\[Python] main.py**');
+ expect(md).toContain('**\\[Ruby] config.rb**');
});
it('skips export block with no pre element', () => {
diff --git a/scripts/rehype-expand-code-tabs.mjs b/scripts/rehype-expand-code-tabs.mjs
index 44183cc14d0aba..02f4a4193b6133 100644
--- a/scripts/rehype-expand-code-tabs.mjs
+++ b/scripts/rehype-expand-code-tabs.mjs
@@ -13,8 +13,9 @@ import {visit} from 'unist-util-visit';
* 1. Finds parent elements that contain [data-code-tab-title] children
* 2. Replaces ALL children with expanded export blocks (removing the
* CodeTabs-rendered content to avoid duplication)
- * 3. Each export block becomes a bold heading + fenced code block,
- * preferring filename over tab title for the heading
+ * 3. Each export block becomes a bold heading + fenced code block.
+ * The heading format is "[Tab Title] filename" when both exist,
+ * or just the tab title / filename when only one is present
*/
export function rehypeExpandCodeTabs() {
return tree => {
@@ -33,7 +34,8 @@ export function rehypeExpandCodeTabs() {
node.children = exportBlocks.flatMap(block => {
const title = block.properties.dataCodeTabTitle;
const filename = block.properties.dataCodeTabFilename;
- const label = filename || title;
+ const label =
+ filename && title ? `[${title}] ${filename}` : filename || title;
const preElements = collectAll(block, el => el.tagName === 'pre');
if (preElements.length === 0) {
From 5a53382cdf1cd1bbbde28e05557ec710020388e2 Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Wed, 13 May 2026 16:52:56 +0200
Subject: [PATCH 7/9] improve tests
---
scripts/generate-md-exports.test.mjs | 116 +++++++++++++--------------
1 file changed, 55 insertions(+), 61 deletions(-)
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index 3590570f9455cc..27ae4c856b8137 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -26,28 +26,6 @@ function htmlToMarkdown(html) {
);
}
-/**
- * Builds HTML matching what the remark-code-tabs plugin + CodeTabs component
- * produce in the static build output:
- *
- *
- *
- *
- *
- *
- * file1
- * code1
- *
- *
- *
- *
- * code1
- *
- *
- * code2
- *
- *
- */
function buildCodeTabsHTML(tabs) {
const firstTab = tabs[0];
@@ -75,7 +53,7 @@ function buildCodeTabsHTML(tabs) {
}
describe('rehypeExpandCodeTabs', () => {
- it('replaces wrapper content with all tabs, not just the active one', () => {
+ it('outputs one fenced code block per tab with "[Title] filename" headings', () => {
const html = buildCodeTabsHTML([
{
title: 'Cloudflare Workers',
@@ -98,18 +76,18 @@ describe('rehypeExpandCodeTabs', () => {
]);
const md = htmlToMarkdown(html);
- const codeBlocks = md.match(/```[\s\S]*?```/g);
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
expect(codeBlocks).toHaveLength(3);
- expect(md).toContain('**\\[Cloudflare Workers] index.ts**');
- expect(md).toContain('**\\[Node.js] app.ts**');
- expect(md).toContain('**\\[Bun] index.ts**');
expect(codeBlocks[0]).toContain('@sentry/hono/cloudflare');
expect(codeBlocks[1]).toContain('@sentry/hono/node');
expect(codeBlocks[2]).toContain('@sentry/hono/bun');
+ expect(md).toContain('**\\[Cloudflare Workers] index.ts**');
+ expect(md).toContain('**\\[Node.js] app.ts**');
+ expect(md).toContain('**\\[Bun] index.ts**');
});
- it('removes CodeTabs-rendered content (tab buttons, filename display, active tab)', () => {
+ it('removes the CodeTabs-rendered active tab to avoid duplication', () => {
const html = buildCodeTabsHTML([
{title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'},
{title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'},
@@ -120,31 +98,37 @@ describe('rehypeExpandCodeTabs', () => {
expect(md).not.toContain('`instrument.mjs`');
});
- it('includes both tab title and filename in heading when both exist', () => {
+ it('uses tab title alone when filename is absent', () => {
const html = buildCodeTabsHTML([
- {title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'init();'},
- {title: 'CommonJS', filename: 'instrument.js', lang: 'javascript', code: 'init();'},
+ {title: 'Cloudflare Workers', lang: 'javascript', code: 'workers();'},
+ {title: 'Bun', lang: 'javascript', code: 'bun();'},
]);
const md = htmlToMarkdown(html);
- expect(md).toContain('**\\[ESM] instrument.mjs**');
- expect(md).toContain('**\\[CommonJS] instrument.js**');
+ const headings = md.match(/\*\*.*?\*\*/g);
+ expect(headings).toHaveLength(2);
+ expect(headings[0]).toBe('**Cloudflare Workers**');
+ expect(headings[1]).toBe('**Bun**');
});
- it('falls back to tab title when no filename is set', () => {
- const html = buildCodeTabsHTML([
- {title: 'Cloudflare Workers', lang: 'javascript', code: 'workers();'},
- {title: 'Bun', lang: 'javascript', code: 'bun();'},
- ]);
+ it('treats empty filename attribute the same as missing filename', () => {
+ const html =
+ '' +
+ '' +
+ 'active()
' +
+ '' +
+ 'hello();
' +
+ '';
const md = htmlToMarkdown(html);
- expect(md).toContain('**Cloudflare Workers**');
- expect(md).toContain('**Bun**');
+ const headings = md.match(/\*\*.*?\*\*/g);
+ expect(headings).toHaveLength(1);
+ expect(headings[0]).toBe('**JavaScript**');
});
- it('does not affect code blocks outside of tab wrappers', () => {
+ it('does not modify code blocks outside tab wrappers', () => {
const html =
'curl -sL https://sentry.io/get-cli/ | bash
';
@@ -152,25 +136,19 @@ describe('rehypeExpandCodeTabs', () => {
const codeBlocks = md.match(/```[\s\S]*?```/g);
expect(codeBlocks).toHaveLength(1);
- expect(md).not.toMatch(/\*\*.*\*\*\n/);
expect(codeBlocks[0]).toContain('curl -sL');
+ expect(md).not.toMatch(/\*\*.*\*\*\n/);
});
- it('keeps standalone blocks intact when mixed with tabs on the same page', () => {
+ it('preserves standalone code blocks when mixed with tab groups', () => {
const standalone =
'npm install @sentry/node
';
const tabs = buildCodeTabsHTML([
- {
- title: 'Node.js',
- filename: 'instrument.mjs',
- lang: 'javascript',
- code: 'Sentry.init();',
- },
+ {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'},
{title: 'Bun', lang: 'javascript', code: 'init();'},
]);
- const html = `${standalone}${tabs}`;
- const md = htmlToMarkdown(html);
+ const md = htmlToMarkdown(`${standalone}${tabs}`);
const codeBlocks = md.match(/```[\s\S]*?```/g);
expect(codeBlocks).toHaveLength(3);
@@ -179,7 +157,7 @@ describe('rehypeExpandCodeTabs', () => {
expect(md).toContain('**Bun**');
});
- it('handles two separate tab groups on the same page', () => {
+ it('expands multiple tab groups independently on the same page', () => {
const group1 = buildCodeTabsHTML([
{title: 'ESM', filename: 'instrument.mjs', lang: 'javascript', code: 'import init'},
{title: 'CJS', filename: 'instrument.js', lang: 'javascript', code: 'require init'},
@@ -188,19 +166,18 @@ describe('rehypeExpandCodeTabs', () => {
{title: 'Python', filename: 'main.py', lang: 'python', code: 'import sentry_sdk'},
{title: 'Ruby', filename: 'config.rb', lang: 'ruby', code: 'require "sentry-ruby"'},
]);
- const html = `${group1}${group2}`;
- const md = htmlToMarkdown(html);
+ const md = htmlToMarkdown(`${group1}${group2}`);
const codeBlocks = md.match(/```[\s\S]*?```/g);
expect(codeBlocks).toHaveLength(4);
- expect(md).toContain('**\\[ESM] instrument.mjs**');
- expect(md).toContain('**\\[CJS] instrument.js**');
- expect(md).toContain('**\\[Python] main.py**');
- expect(md).toContain('**\\[Ruby] config.rb**');
+ expect(codeBlocks[0]).toContain('import init');
+ expect(codeBlocks[1]).toContain('require init');
+ expect(codeBlocks[2]).toContain('import sentry_sdk');
+ expect(codeBlocks[3]).toContain('require "sentry-ruby"');
});
- it('skips export block with no pre element', () => {
+ it('drops export blocks that contain no pre element', () => {
const html =
'' +
'active tab
' +
@@ -210,9 +187,26 @@ describe('rehypeExpandCodeTabs', () => {
const md = htmlToMarkdown(html);
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(1);
+ expect(codeBlocks[0]).toContain('works()');
expect(md).toContain('**ok**');
- expect(md).toContain('works()');
- expect(md).not.toContain('**broken**');
+ expect(md).not.toContain('broken');
expect(md).not.toContain('active tab');
});
+
+ it('leaves wrapper unchanged when it has no export blocks', () => {
+ const html =
+ '' +
+ '' +
+ 'solo();
' +
+ '' +
+ '';
+
+ const md = htmlToMarkdown(html);
+
+ const codeBlocks = md.match(/```[\s\S]*?```/g);
+ expect(codeBlocks).toHaveLength(1);
+ expect(codeBlocks[0]).toContain('solo()');
+ });
});
From cbf4d1d259308d07ab5bad26acca10f4da32e3d5 Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Wed, 13 May 2026 16:53:08 +0200
Subject: [PATCH 8/9] prettier
---
scripts/generate-md-exports.test.mjs | 7 ++++++-
scripts/rehype-expand-code-tabs.mjs | 3 +--
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/scripts/generate-md-exports.test.mjs b/scripts/generate-md-exports.test.mjs
index 27ae4c856b8137..bca320add3be49 100644
--- a/scripts/generate-md-exports.test.mjs
+++ b/scripts/generate-md-exports.test.mjs
@@ -144,7 +144,12 @@ describe('rehypeExpandCodeTabs', () => {
const standalone =
'npm install @sentry/node
';
const tabs = buildCodeTabsHTML([
- {title: 'Node.js', filename: 'instrument.mjs', lang: 'javascript', code: 'Sentry.init();'},
+ {
+ title: 'Node.js',
+ filename: 'instrument.mjs',
+ lang: 'javascript',
+ code: 'Sentry.init();',
+ },
{title: 'Bun', lang: 'javascript', code: 'init();'},
]);
diff --git a/scripts/rehype-expand-code-tabs.mjs b/scripts/rehype-expand-code-tabs.mjs
index 02f4a4193b6133..f6f0a528cffa34 100644
--- a/scripts/rehype-expand-code-tabs.mjs
+++ b/scripts/rehype-expand-code-tabs.mjs
@@ -34,8 +34,7 @@ export function rehypeExpandCodeTabs() {
node.children = exportBlocks.flatMap(block => {
const title = block.properties.dataCodeTabTitle;
const filename = block.properties.dataCodeTabFilename;
- const label =
- filename && title ? `[${title}] ${filename}` : filename || title;
+ const label = filename && title ? `[${title}] ${filename}` : filename || title;
const preElements = collectAll(block, el => el.tagName === 'pre');
if (preElements.length === 0) {
From 67985d9e9a6f4c4b81f770c78c143d2f4e8bf8c6 Mon Sep 17 00:00:00 2001
From: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
Date: Wed, 13 May 2026 17:27:11 +0200
Subject: [PATCH 9/9] increment cache version
---
scripts/generate-md-exports.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/generate-md-exports.mjs b/scripts/generate-md-exports.mjs
index 95c5a67e360f16..a99a21ec852abc 100644
--- a/scripts/generate-md-exports.mjs
+++ b/scripts/generate-md-exports.mjs
@@ -32,7 +32,7 @@ import {rehypeExpandCodeTabs} from './rehype-expand-code-tabs.mjs';
const DOCS_ORIGIN = process.env.NEXT_PUBLIC_DEVELOPER_DOCS
? 'https://develop.sentry.dev'
: 'https://docs.sentry.io';
-const CACHE_VERSION = 7;
+const CACHE_VERSION = 8;
const CACHE_COMPRESS_LEVEL = 4;
const R2_BUCKET = process.env.NEXT_PUBLIC_DEVELOPER_DOCS
? 'sentry-develop-docs'