diff --git a/data/onPostBuild/transpileMdxToMarkdown.test.ts b/data/onPostBuild/transpileMdxToMarkdown.test.ts
index b92dcf3928..86fcdd759b 100644
--- a/data/onPostBuild/transpileMdxToMarkdown.test.ts
+++ b/data/onPostBuild/transpileMdxToMarkdown.test.ts
@@ -6,6 +6,8 @@ import {
removeJsxComments,
convertImagePathsToGitHub,
convertDocsLinksToMarkdown,
+ convertJsxLinkProps,
+ generateLinkTextFromPath,
convertRelativeUrls,
replaceTemplateVariables,
calculateOutputPath,
@@ -437,6 +439,110 @@ import Baz from 'qux';
});
});
+ describe('generateLinkTextFromPath', () => {
+ it('should handle /docs root path without extra "docs"', () => {
+ const output = generateLinkTextFromPath('/docs');
+ expect(output).toBe('ably docs');
+ });
+
+ it('should convert /docs/ path to readable text', () => {
+ const output = generateLinkTextFromPath('/docs/chat/getting-started/javascript');
+ expect(output).toBe('ably docs chat getting-started javascript');
+ });
+
+ it('should handle trailing slashes without producing trailing spaces', () => {
+ const output = generateLinkTextFromPath('/docs/channels/');
+ expect(output).toBe('ably docs channels');
+ });
+ });
+
+ describe('convertJsxLinkProps', () => {
+ it('should convert quoted /docs/ path with single quotes', () => {
+ const input = `'/docs/chat/getting-started/javascript'`;
+ const output = convertJsxLinkProps(input, siteUrl);
+ const expected =
+ `'[ably docs chat getting-started javascript]` +
+ `(http://localhost:3000/docs/chat/getting-started/javascript)'`;
+ expect(output).toBe(expected);
+ });
+
+ it('should convert quoted /docs/ path with double quotes', () => {
+ const input = `"/docs/chat/getting-started/react"`;
+ const output = convertJsxLinkProps(input, siteUrl);
+ const expected =
+ `"[ably docs chat getting-started react]` +
+ `(http://localhost:3000/docs/chat/getting-started/react)"`;
+ expect(output).toBe(expected);
+ });
+
+ it('should not convert non-docs paths', () => {
+ const input = `'/blog/article'`;
+ const output = convertJsxLinkProps(input, siteUrl);
+ expect(output).toBe(`'/blog/article'`);
+ });
+
+ it('should not convert external URLs', () => {
+ const input = `'https://example.com/docs/page'`;
+ const output = convertJsxLinkProps(input, siteUrl);
+ expect(output).toBe(`'https://example.com/docs/page'`);
+ });
+
+ it('should handle Tiles component content like the real example', () => {
+ const input = `
+{[
+ {
+ title: 'JavaScript',
+ description: 'Start building with Chat using Ably\\'s JavaScript SDK',
+ image: 'icon-tech-javascript',
+ link: '/docs/chat/getting-started/javascript',
+ },
+ {
+ title: 'React',
+ description: 'Start building Chat applications using Ably\\'s React SDK.',
+ image: 'icon-tech-react',
+ link: '/docs/chat/getting-started/react',
+ },
+]}
+`;
+ const output = convertJsxLinkProps(input, siteUrl);
+ const expectedJsLink =
+ `link: '[ably docs chat getting-started javascript]` +
+ `(http://localhost:3000/docs/chat/getting-started/javascript)'`;
+ const expectedReactLink =
+ `link: '[ably docs chat getting-started react]` +
+ `(http://localhost:3000/docs/chat/getting-started/react)'`;
+ expect(output).toContain(expectedJsLink);
+ expect(output).toContain(expectedReactLink);
+ // Ensure other content is preserved
+ expect(output).toContain(`title: 'JavaScript'`);
+ expect(output).toContain(`image: 'icon-tech-javascript'`);
+ });
+
+ it('should not convert /docs/ paths inside code blocks', () => {
+ const input = `Here's an example:
+
+\`\`\`ruby
+link '/docs/channels'
+\`\`\`
+
+\`\`\`javascript
+fetch('/docs/api/posts/123')
+await fetch("/docs/api/posts/456")
+\`\`\`
+
+Real prop: link: '/docs/presence'`;
+ const output = convertJsxLinkProps(input, siteUrl);
+ // Code block content should be preserved
+ expect(output).toContain("link '/docs/channels'");
+ expect(output).toContain("fetch('/docs/api/posts/123')");
+ expect(output).toContain('fetch("/docs/api/posts/456")');
+ // Non-code-block content should be converted
+ const expectedPresenceLink =
+ `link: '[ably docs presence](http://localhost:3000/docs/presence)'`;
+ expect(output).toContain(expectedPresenceLink);
+ });
+ });
+
describe('convertRelativeUrls', () => {
it('should convert relative URLs to absolute', () => {
const input = '[Link text](/docs/channels)';
diff --git a/data/onPostBuild/transpileMdxToMarkdown.ts b/data/onPostBuild/transpileMdxToMarkdown.ts
index 2ad308ef9e..ffcacdc569 100644
--- a/data/onPostBuild/transpileMdxToMarkdown.ts
+++ b/data/onPostBuild/transpileMdxToMarkdown.ts
@@ -32,6 +32,51 @@ interface FrontMatterAttributes {
[key: string]: any;
}
+/**
+ * Split content into code block and non-code-block sections
+ * Used to preserve code blocks during content transformations
+ */
+function splitByCodeBlocks(content: string): Array<{ content: string; isCodeBlock: boolean }> {
+ const parts: Array<{ content: string; isCodeBlock: boolean }> = [];
+ const fenceRegex = /```[\s\S]*?```/g;
+
+ let lastIndex = 0;
+ const matches = Array.from(content.matchAll(fenceRegex));
+
+ for (const match of matches) {
+ if (match.index !== undefined && match.index > lastIndex) {
+ parts.push({
+ content: content.slice(lastIndex, match.index),
+ isCodeBlock: false,
+ });
+ }
+ parts.push({
+ content: match[0],
+ isCodeBlock: true,
+ });
+ lastIndex = (match.index || 0) + match[0].length;
+ }
+
+ if (lastIndex < content.length) {
+ parts.push({
+ content: content.slice(lastIndex),
+ isCodeBlock: false,
+ });
+ }
+
+ return parts;
+}
+
+/**
+ * Apply a transformation function only to non-code-block content
+ * Preserves fenced code blocks (``` ... ```) exactly as-is
+ */
+function transformNonCodeBlocks(content: string, transform: (text: string) => string): string {
+ return splitByCodeBlocks(content)
+ .map((part) => (part.isCodeBlock ? part.content : transform(part.content)))
+ .join('');
+}
+
/**
* Remove import and export statements from content
* Uses a line-by-line parser that only removes import/export from the top of the file,
@@ -148,47 +193,9 @@ function removeImportExportStatements(content: string): string {
* Remove script tags that are not inside code blocks
*/
function removeScriptTags(content: string): string {
- // Split content into code block and non-code-block sections
- const parts: Array<{ content: string; isCodeBlock: boolean }> = [];
- const fenceRegex = /```[\s\S]*?```/g;
-
- let lastIndex = 0;
- const matches = Array.from(content.matchAll(fenceRegex));
-
- for (const match of matches) {
- // Add content before code block
- if (match.index !== undefined && match.index > lastIndex) {
- parts.push({
- content: content.slice(lastIndex, match.index),
- isCodeBlock: false,
- });
- }
- // Add code block itself
- parts.push({
- content: match[0],
- isCodeBlock: true,
- });
- lastIndex = (match.index || 0) + match[0].length;
- }
-
- // Add remaining content after last code block
- if (lastIndex < content.length) {
- parts.push({
- content: content.slice(lastIndex),
- isCodeBlock: false,
- });
- }
-
- // Remove script tags only from non-code-block parts
- return parts
- .map((part) => {
- if (part.isCodeBlock) {
- return part.content; // Preserve code blocks exactly
- }
- // Remove script tags with any attributes and their content
- return part.content.replace(/