From bb8c72624ff04675dee548b6f398206a037e8126 Mon Sep 17 00:00:00 2001
From: Dan <37590801+DanOnCall@users.noreply.github.com>
Date: Thu, 3 Jul 2025 16:43:30 -0400
Subject: [PATCH 1/9] refactor: Update styling and add copy as Markdown feature
---
src/app/homepage/homepage.component.scss | 36 +++++++++++++++----
src/app/homepage/pages/page/page.component.ts | 10 ++++++
tools/transforms/content-package/index.ts | 4 +++
.../content-package/readers/content.ts | 9 +++--
.../rendering/filters/base64.ts | 8 +++++
.../processors/convertToJson.ts | 9 +++--
.../templates/content.template.html | 30 +++++++++++-----
7 files changed, 83 insertions(+), 23 deletions(-)
create mode 100644 tools/transforms/content-package/rendering/filters/base64.ts
diff --git a/src/app/homepage/homepage.component.scss b/src/app/homepage/homepage.component.scss
index fbfccb1346..31ca187b8a 100644
--- a/src/app/homepage/homepage.component.scss
+++ b/src/app/homepage/homepage.component.scss
@@ -77,7 +77,9 @@
}
}
- .content blockquote strong, .content #carbonads a, .content blockquote a {
+ .content blockquote strong,
+ .content #carbonads a,
+ .content blockquote a {
-webkit-text-fill-color: unset !important;
-webkit-background-clip: unset !important;
background: none;
@@ -258,17 +260,16 @@
}
&::before {
- content: "";
+ content: '';
position: absolute;
inset: 0;
border-radius: 2px;
padding: 2px;
background: var(--primary-gradient);
- -webkit-mask:
- linear-gradient(#fff 0 0) content-box,
+ -webkit-mask: linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
- mask-composite: exclude;
+ mask-composite: exclude;
pointer-events: none;
z-index: 2;
}
@@ -432,10 +433,14 @@
}
}
-.github-links {
+.content-actions {
float: right;
margin-top: 24px;
+ display: flex;
+ gap: 8px;
+}
+.github-links {
svg {
font-size: 17px;
color: var(--color-1dp);
@@ -446,6 +451,23 @@
}
}
+.content-buttons {
+ button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ }
+
+ svg {
+ font-size: 17px;
+ color: var(--color-1dp);
+ }
+
+ button:hover svg {
+ color: var(--primary);
+ }
+}
+
.toc-wrapper {
@include utils.media(large) {
display: none;
@@ -465,7 +487,7 @@
-webkit-filter: var(--company-filter);
filter: var(--company-filter);
opacity: var(--company-logo-opacity);
-
+
&:hover {
-webkit-filter: var(--company-filter-hover);
filter: var(--company-filter-hover);
diff --git a/src/app/homepage/pages/page/page.component.ts b/src/app/homepage/pages/page/page.component.ts
index bda9ecc98f..882e30cea9 100644
--- a/src/app/homepage/pages/page/page.component.ts
+++ b/src/app/homepage/pages/page/page.component.ts
@@ -32,6 +32,16 @@ export class BasePageComponent implements AfterViewChecked {
return this.isHljsInitialized;
}
+ copyAsMarkdown() {
+ const rawMarkdownEl = this.el.nativeElement.querySelector('#raw-markdown');
+ if (!rawMarkdownEl) {
+ return;
+ }
+ const encoded = rawMarkdownEl.textContent;
+ const decoded = atob(encoded);
+ navigator.clipboard.writeText(decoded);
+ }
+
ngAfterViewChecked() {
this.initHljs();
}
diff --git a/tools/transforms/content-package/index.ts b/tools/transforms/content-package/index.ts
index 7c1dc19685..eec47ca6e0 100644
--- a/tools/transforms/content-package/index.ts
+++ b/tools/transforms/content-package/index.ts
@@ -6,6 +6,7 @@ import {
extractContentTitleProcessor,
} from './processors';
import { ContentFileReader, contentFileReader } from './readers';
+import { base64NunjucksFilter } from './rendering/filters/base64';
import { nestjsMarkedNunjucksFilter } from './rendering/filters/nestjs-marked';
import { nestjsMarkedNunjucksTag } from './rendering/tags/nestjs-marked';
import { renderNestJSMarkdown } from './services';
@@ -15,6 +16,7 @@ export default new Package('content', [])
.factory(renderNestJSMarkdown)
.factory(nestjsMarkedNunjucksTag)
.factory(nestjsMarkedNunjucksFilter)
+ .factory(base64NunjucksFilter)
.processor(extractContentTitleProcessor)
.processor(computeOutputPathProcessor)
.processor(computeWhoUsesProcessor)
@@ -26,9 +28,11 @@ export default new Package('content', [])
templateEngine: any,
nestjsMarkedNunjucksTag: any,
nestjsMarkedNunjucksFilter: any,
+ base64NunjucksFilter: any,
) => {
templateEngine.tags.push(nestjsMarkedNunjucksTag);
templateEngine.filters.push(nestjsMarkedNunjucksFilter);
+ templateEngine.filters.push(base64NunjucksFilter);
},
)
.config((computeIdsProcessor) => {
diff --git a/tools/transforms/content-package/readers/content.ts b/tools/transforms/content-package/readers/content.ts
index 9aa208c926..0d302d1eb5 100644
--- a/tools/transforms/content-package/readers/content.ts
+++ b/tools/transforms/content-package/readers/content.ts
@@ -9,11 +9,10 @@ export class ContentFileReader implements FileReader {
getDocs(fileInfo: FileInfo) {
return [
{
- docType: fileInfo.baseName === 'who-uses'
- ? 'who-uses'
- : 'content',
- content: fileInfo.content
- }
+ docType: fileInfo.baseName === 'who-uses' ? 'who-uses' : 'content',
+ content: fileInfo.content,
+ rawContent: fileInfo.content,
+ },
];
}
}
diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts
new file mode 100644
index 0000000000..11af0269d9
--- /dev/null
+++ b/tools/transforms/content-package/rendering/filters/base64.ts
@@ -0,0 +1,8 @@
+export function base64NunjucksFilter() {
+ return {
+ name: 'base64',
+ process(str: string) {
+ return Buffer.from(str).toString('base64');
+ },
+ };
+}
diff --git a/tools/transforms/nestjs-base-package/processors/convertToJson.ts b/tools/transforms/nestjs-base-package/processors/convertToJson.ts
index af0a44d7a6..5228b89893 100644
--- a/tools/transforms/nestjs-base-package/processors/convertToJson.ts
+++ b/tools/transforms/nestjs-base-package/processors/convertToJson.ts
@@ -4,10 +4,13 @@ import { CreateDocMessage, DgeniLogger } from '../../shared';
/**
* Converts doc types set by the `docTypes` property to JSON documents.
* A document requires `doc.renderContent` and `doc.title` or `doc.name`
- * property.
+ * property.
*/
export class ConvertToJsonProcessor implements Processor {
- constructor(private log: DgeniLogger, private createDocMessage: CreateDocMessage) {}
+ constructor(
+ private log: DgeniLogger,
+ private createDocMessage: CreateDocMessage,
+ ) {}
$runAfter = ['postProcessHtml'];
$runBefore = ['writeFilesProcessor'];
docTypes = [];
@@ -32,7 +35,7 @@ export class ConvertToJsonProcessor implements Processor {
doc.renderedContent = JSON.stringify(
{ id: doc.path, title, contents },
null,
- 2
+ 2,
);
}
});
diff --git a/tools/transforms/templates/content.template.html b/tools/transforms/templates/content.template.html
index d4d8833475..eebf28c9dc 100644
--- a/tools/transforms/templates/content.template.html
+++ b/tools/transforms/templates/content.template.html
@@ -1,13 +1,27 @@
{% block content %}
-
-
-
-
+
+ {$ doc.rawContent | base64 $}
+
+
{$ doc.description | nestjsmarked $}
From 493f997209d17263f2fd826fd03c3613a0c27809 Mon Sep 17 00:00:00 2001
From: Dan <37590801+DanOnCall@users.noreply.github.com>
Date: Thu, 3 Jul 2025 20:55:36 -0400
Subject: [PATCH 2/9] refactor: add states to copy button
---
src/app/homepage/homepage.component.scss | 26 ++++++-
src/app/homepage/pages/page/page.component.ts | 78 ++++++++++++++++++-
src/scss/theme.scss | 18 ++++-
.../templates/content.template.html | 5 +-
4 files changed, 117 insertions(+), 10 deletions(-)
diff --git a/src/app/homepage/homepage.component.scss b/src/app/homepage/homepage.component.scss
index 31ca187b8a..d6a313d2e3 100644
--- a/src/app/homepage/homepage.component.scss
+++ b/src/app/homepage/homepage.component.scss
@@ -437,7 +437,7 @@
float: right;
margin-top: 24px;
display: flex;
- gap: 8px;
+ gap: 16px;
}
.github-links {
@@ -456,18 +456,38 @@
background: none;
border: none;
cursor: pointer;
+ padding: 0;
}
svg {
font-size: 17px;
- color: var(--color-1dp);
}
+}
- button:hover svg {
+.content-action {
+ color: var(--color-1dp);
+
+ &:hover {
color: var(--primary);
}
}
+.content-action--success {
+ color: var(--success);
+
+ &:hover {
+ color: var(--success);
+ }
+}
+
+.content-action--error {
+ color: var(--error);
+
+ &:hover {
+ color: var(--error);
+ }
+}
+
.toc-wrapper {
@include utils.media(large) {
display: none;
diff --git a/src/app/homepage/pages/page/page.component.ts b/src/app/homepage/pages/page/page.component.ts
index 882e30cea9..3316884391 100644
--- a/src/app/homepage/pages/page/page.component.ts
+++ b/src/app/homepage/pages/page/page.component.ts
@@ -3,6 +3,7 @@ import {
ApplicationRef,
Component,
ElementRef,
+ OnDestroy,
} from '@angular/core';
import * as Prism from 'prismjs';
import 'prismjs/prism';
@@ -16,8 +17,10 @@ import 'prismjs/components/prism-bash';
selector: 'app-base-page',
template: ``,
})
-export class BasePageComponent implements AfterViewChecked {
+export class BasePageComponent implements AfterViewChecked, OnDestroy {
private isHljsInitialized = false;
+ public copyButtonState: 'idle' | 'copied' | 'error' = 'idle';
+ private copyResetTimeout: number | null = null;
constructor(
private readonly applicationRef: ApplicationRef,
@@ -32,14 +35,83 @@ export class BasePageComponent implements AfterViewChecked {
return this.isHljsInitialized;
}
- copyAsMarkdown() {
+ private isElement(target: EventTarget | null): target is Element {
+ return target instanceof Element;
+ }
+
+ async copyAsMarkdown(event: Event) {
+ if (!this.isElement(event.target)) {
+ return;
+ }
+
+ const buttonElement = event.target.closest('button');
+ if (!buttonElement) {
+ return;
+ }
+
const rawMarkdownEl = this.el.nativeElement.querySelector('#raw-markdown');
if (!rawMarkdownEl) {
return;
}
+
const encoded = rawMarkdownEl.textContent;
const decoded = atob(encoded);
- navigator.clipboard.writeText(decoded);
+
+ if (!navigator.clipboard) {
+ this.copyButtonState = 'error';
+ buttonElement.classList.add('content-action--error');
+ buttonElement.innerHTML = '
';
+ buttonElement.setAttribute('aria-label', 'Copy failed');
+ buttonElement.setAttribute('title', 'Copy failed');
+ return;
+ }
+
+ await navigator.clipboard.writeText(decoded);
+
+ this.copyButtonState = 'copied';
+
+ // Add success state class and replace with checkmark
+ buttonElement.classList.add('content-action--success');
+ buttonElement.innerHTML = '
';
+ buttonElement.setAttribute('aria-label', 'Copied');
+ buttonElement.setAttribute('title', 'Copied');
+ }
+
+ onCopyButtonMouseLeave(event: Event) {
+ if (this.copyButtonState === 'idle') {
+ return;
+ }
+
+ if (this.copyResetTimeout) {
+ clearTimeout(this.copyResetTimeout);
+ }
+
+ this.copyResetTimeout = setTimeout(() => {
+ this.copyButtonState = 'idle';
+
+ if (!this.isElement(event.target)) {
+ return;
+ }
+
+ const buttonElement = event.target.closest('button');
+ if (!buttonElement) {
+ return;
+ }
+
+ // Reset to copy icon
+ buttonElement.className = 'content-action';
+ buttonElement.innerHTML = '
';
+ buttonElement.setAttribute('aria-label', 'Copy as Markdown');
+ buttonElement.setAttribute('title', 'Copy as Markdown');
+ this.copyResetTimeout = null;
+ }, 500) as unknown as number;
+ }
+
+ ngOnDestroy() {
+ // Clean up timeout if component is destroyed
+ if (this.copyResetTimeout) {
+ clearTimeout(this.copyResetTimeout);
+ }
}
ngAfterViewChecked() {
diff --git a/src/scss/theme.scss b/src/scss/theme.scss
index d0a3f29414..27eb88b251 100644
--- a/src/scss/theme.scss
+++ b/src/scss/theme.scss
@@ -3,10 +3,14 @@
--primary-accent: #ea2868;
--primary-1dp: #d71e38;
--primary-2dp: #da2640;
- --primary-3dp:#db2840;
+ --primary-3dp: #db2840;
--primary-4dp: #e40020;
--primary-5dp: #ff0023;
- --primary-gradient: linear-gradient(90deg, var(--primary) 0%, var(--primary-accent) 100%);
+ --primary-gradient: linear-gradient(
+ 90deg,
+ var(--primary) 0%,
+ var(--primary-accent) 100%
+ );
--color: #404040;
--color-1dp: #151515;
@@ -38,6 +42,8 @@
--error: #ed2945;
--error-background: #f9eff1;
+ --success: #28a745;
+
--company-filter: grayscale(100%);
--company-filter-hover: grayscale(0%);
--company-logo-filter: grayscale(1);
@@ -56,7 +62,11 @@
--primary-3dp: #f1455f;
--primary-4dp: #f23c57;
--primary-5dp: #f23551;
- --primary-gradient: linear-gradient(90deg, var(--primary) 0%, var(--primary-accent) 100%);
+ --primary-gradient: linear-gradient(
+ 90deg,
+ var(--primary) 0%,
+ var(--primary-accent) 100%
+ );
--color: #dfdfe3;
--color-1dp: #d0d0d4;
@@ -88,6 +98,8 @@
--error: #ff677c;
--error-background: #3a2f30;
+ --success: #28a745;
+
--company-filter: contrast(0.5);
--company-filter-hover: opacity(1);
--company-logo-filter: contrast(0.5) grayscale(100%);
diff --git a/tools/transforms/templates/content.template.html b/tools/transforms/templates/content.template.html
index eebf28c9dc..99a5aa8eb3 100644
--- a/tools/transforms/templates/content.template.html
+++ b/tools/transforms/templates/content.template.html
@@ -6,9 +6,12 @@
From ecafa25fcdd9ac4fc224d747e6b2ad8ee3b41d85 Mon Sep 17 00:00:00 2001
From: Dan <37590801+DanOnCall@users.noreply.github.com>
Date: Thu, 3 Jul 2025 21:12:22 -0400
Subject: [PATCH 3/9] feat: add domain to all images to make content portable
---
.../content-package/rendering/filters/base64.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts
index 11af0269d9..93f9a5322e 100644
--- a/tools/transforms/content-package/rendering/filters/base64.ts
+++ b/tools/transforms/content-package/rendering/filters/base64.ts
@@ -2,7 +2,14 @@ export function base64NunjucksFilter() {
return {
name: 'base64',
process(str: string) {
- return Buffer.from(str).toString('base64');
+ // Fix relative URLs to absolute URLs for copying
+ const fixedMarkdown = str
+ // Fix image sources: /assets/... → https://docs.nestjs.com/assets/...
+ .replace(/src="(\/[^"]+)"/g, 'src="https://docs.nestjs.com$1"')
+ // Fix documentation links: [text](/some/path) → [text](https://docs.nestjs.com/some/path)
+ .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)');
+
+ return Buffer.from(fixedMarkdown).toString('base64');
},
};
}
From 0ad6b659e50745d8dcd4bd8bbfed9f970721a912 Mon Sep 17 00:00:00 2001
From: Dan <37590801+DanOnCall@users.noreply.github.com>
Date: Thu, 3 Jul 2025 21:19:17 -0400
Subject: [PATCH 4/9] feat: remove angular comps from markdown
---
.../transforms/content-package/rendering/filters/base64.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts
index 93f9a5322e..d5e5aa26df 100644
--- a/tools/transforms/content-package/rendering/filters/base64.ts
+++ b/tools/transforms/content-package/rendering/filters/base64.ts
@@ -7,7 +7,11 @@ export function base64NunjucksFilter() {
// Fix image sources: /assets/... → https://docs.nestjs.com/assets/...
.replace(/src="(\/[^"]+)"/g, 'src="https://docs.nestjs.com$1"')
// Fix documentation links: [text](/some/path) → [text](https://docs.nestjs.com/some/path)
- .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)');
+ .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)')
+ // Remove custom Angular components for cleaner portable markdown
+ .replace(/
]*><\/app-[^>]*>/g, '')
+ // Remove any empty lines left by component removal
+ .replace(/\n\s*\n\s*\n/g, '\n\n');
return Buffer.from(fixedMarkdown).toString('base64');
},
From 208f8d10cfc19232ae57419fdea4b38259f3f05c Mon Sep 17 00:00:00 2001
From: Dan <37590801+DanOnCall@users.noreply.github.com>
Date: Thu, 3 Jul 2025 21:22:55 -0400
Subject: [PATCH 5/9] feat: rename filter
---
tools/transforms/content-package/index.ts | 8 ++++----
.../content-package/rendering/filters/base64.ts | 4 ++--
tools/transforms/templates/content.template.html | 2 +-
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/tools/transforms/content-package/index.ts b/tools/transforms/content-package/index.ts
index eec47ca6e0..7a870335bd 100644
--- a/tools/transforms/content-package/index.ts
+++ b/tools/transforms/content-package/index.ts
@@ -6,7 +6,7 @@ import {
extractContentTitleProcessor,
} from './processors';
import { ContentFileReader, contentFileReader } from './readers';
-import { base64NunjucksFilter } from './rendering/filters/base64';
+import { cleanMarkdownNunjucksFilter } from './rendering/filters/base64';
import { nestjsMarkedNunjucksFilter } from './rendering/filters/nestjs-marked';
import { nestjsMarkedNunjucksTag } from './rendering/tags/nestjs-marked';
import { renderNestJSMarkdown } from './services';
@@ -16,7 +16,7 @@ export default new Package('content', [])
.factory(renderNestJSMarkdown)
.factory(nestjsMarkedNunjucksTag)
.factory(nestjsMarkedNunjucksFilter)
- .factory(base64NunjucksFilter)
+ .factory(cleanMarkdownNunjucksFilter)
.processor(extractContentTitleProcessor)
.processor(computeOutputPathProcessor)
.processor(computeWhoUsesProcessor)
@@ -28,11 +28,11 @@ export default new Package('content', [])
templateEngine: any,
nestjsMarkedNunjucksTag: any,
nestjsMarkedNunjucksFilter: any,
- base64NunjucksFilter: any,
+ cleanMarkdownNunjucksFilter: any,
) => {
templateEngine.tags.push(nestjsMarkedNunjucksTag);
templateEngine.filters.push(nestjsMarkedNunjucksFilter);
- templateEngine.filters.push(base64NunjucksFilter);
+ templateEngine.filters.push(cleanMarkdownNunjucksFilter);
},
)
.config((computeIdsProcessor) => {
diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts
index d5e5aa26df..68f49e9243 100644
--- a/tools/transforms/content-package/rendering/filters/base64.ts
+++ b/tools/transforms/content-package/rendering/filters/base64.ts
@@ -1,6 +1,6 @@
-export function base64NunjucksFilter() {
+export function cleanMarkdownNunjucksFilter() {
return {
- name: 'base64',
+ name: 'cleanMarkdown',
process(str: string) {
// Fix relative URLs to absolute URLs for copying
const fixedMarkdown = str
diff --git a/tools/transforms/templates/content.template.html b/tools/transforms/templates/content.template.html
index 99a5aa8eb3..eb5e2d957b 100644
--- a/tools/transforms/templates/content.template.html
+++ b/tools/transforms/templates/content.template.html
@@ -1,7 +1,7 @@
{% block content %}
- {$ doc.rawContent | base64 $}
+ {$ doc.rawContent | cleanMarkdown $}
From 638dfd61a400d1507c5d3ca94530bf4b710b1643 Mon Sep 17 00:00:00 2001
From: Dan <37590801+DanOnCall@users.noreply.github.com>
Date: Thu, 3 Jul 2025 21:58:16 -0400
Subject: [PATCH 6/9] feat: parse @@filename and @@switch
---
.../rendering/filters/base64.ts | 22 ++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts
index 68f49e9243..e7590035d7 100644
--- a/tools/transforms/content-package/rendering/filters/base64.ts
+++ b/tools/transforms/content-package/rendering/filters/base64.ts
@@ -2,18 +2,34 @@ export function cleanMarkdownNunjucksFilter() {
return {
name: 'cleanMarkdown',
process(str: string) {
- // Fix relative URLs to absolute URLs for copying
- const fixedMarkdown = str
+ const cleanMarkdown = str
+ // Fix relative URLs to absolute URLs for copying
// Fix image sources: /assets/... → https://docs.nestjs.com/assets/...
.replace(/src="(\/[^"]+)"/g, 'src="https://docs.nestjs.com$1"')
// Fix documentation links: [text](/some/path) → [text](https://docs.nestjs.com/some/path)
.replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)')
// Remove custom Angular components for cleaner portable markdown
.replace(/
]*><\/app-[^>]*>/g, '')
+ // Convert NestJS-specific syntax to standard markdown
+ .replace(/@@filename\(([^)]+)\)/g, (match, filename) => {
+ // Add .ts extension if filename doesn't already have a proper file extension
+ return filename.match(
+ /\.(ts|js|json|html|css|scss|yaml|yml|xml|dockerfile|md)$/i,
+ )
+ ? `// ${filename}`
+ : `// ${filename}.ts`;
+ })
+ // Remove empty filename placeholders
+ .replace(/@@filename\(\)/g, '')
+ // Split TypeScript/JavaScript versions into separate code blocks
+ .replace(
+ /@@switch\n?/g,
+ '```\n\nJavaScript version:\n\n```javascript\n',
+ )
// Remove any empty lines left by component removal
.replace(/\n\s*\n\s*\n/g, '\n\n');
- return Buffer.from(fixedMarkdown).toString('base64');
+ return Buffer.from(cleanMarkdown).toString('base64');
},
};
}
From bd533eb10e76b57738b180f8f13a68e331cc36ee Mon Sep 17 00:00:00 2001
From: Dan <37590801+DanOnCall@users.noreply.github.com>
Date: Fri, 4 Jul 2025 00:20:29 -0400
Subject: [PATCH 7/9] feat: load md files on demand from static assets
---
.gitignore | 3 +
src/app/homepage/pages/page/page.component.ts | 52 ++++++++++------
tools/transforms/content-package/index.ts | 2 +
.../content-package/processors/index.ts | 3 +-
.../processors/outputCleanMarkdown.ts | 61 +++++++++++++++++++
.../templates/content.template.html | 3 -
6 files changed, 103 insertions(+), 21 deletions(-)
create mode 100644 tools/transforms/content-package/processors/outputCleanMarkdown.ts
diff --git a/.gitignore b/.gitignore
index b30333ad72..333630ae3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,3 +57,6 @@ Thumbs.db
# compiled templates
src/app/homepage/pages/**/*.html
+
+# generated markdown content
+src/assets/content/
diff --git a/src/app/homepage/pages/page/page.component.ts b/src/app/homepage/pages/page/page.component.ts
index 3316884391..6b169973c5 100644
--- a/src/app/homepage/pages/page/page.component.ts
+++ b/src/app/homepage/pages/page/page.component.ts
@@ -4,7 +4,11 @@ import {
Component,
ElementRef,
OnDestroy,
+ inject,
} from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Router } from '@angular/router';
+import { firstValueFrom } from 'rxjs';
import * as Prism from 'prismjs';
import 'prismjs/prism';
import 'prismjs/components/prism-typescript';
@@ -19,6 +23,8 @@ import 'prismjs/components/prism-bash';
})
export class BasePageComponent implements AfterViewChecked, OnDestroy {
private isHljsInitialized = false;
+ private readonly http = inject(HttpClient);
+ private readonly router = inject(Router);
public copyButtonState: 'idle' | 'copied' | 'error' = 'idle';
private copyResetTimeout: number | null = null;
@@ -49,34 +55,46 @@ export class BasePageComponent implements AfterViewChecked, OnDestroy {
return;
}
- const rawMarkdownEl = this.el.nativeElement.querySelector('#raw-markdown');
- if (!rawMarkdownEl) {
- return;
- }
+ try {
+ // Get the current route path (matches doc.id from Dgeni)
+ const currentPath = this.router.url.slice(1);
+ const markdownUrl = `/assets/content/${currentPath}.md`;
- const encoded = rawMarkdownEl.textContent;
- const decoded = atob(encoded);
+ // Fetch the cleaned Markdown file
+ const markdown = await firstValueFrom(
+ this.http.get(markdownUrl, { responseType: 'text' }),
+ );
- if (!navigator.clipboard) {
- this.copyButtonState = 'error';
- buttonElement.classList.add('content-action--error');
- buttonElement.innerHTML = '';
- buttonElement.setAttribute('aria-label', 'Copy failed');
- buttonElement.setAttribute('title', 'Copy failed');
- return;
- }
+ const canCopy = markdown && navigator.clipboard;
+ if (!canCopy) {
+ this.setCopyError(buttonElement);
+ return;
+ }
- await navigator.clipboard.writeText(decoded);
+ await navigator.clipboard.writeText(markdown);
+ this.setCopySuccess(buttonElement);
+ } catch (error) {
+ console.error('Failed to copy markdown:', error);
+ this.setCopyError(buttonElement);
+ }
+ }
+ private setCopySuccess(buttonElement: Element) {
this.copyButtonState = 'copied';
-
- // Add success state class and replace with checkmark
buttonElement.classList.add('content-action--success');
buttonElement.innerHTML = '';
buttonElement.setAttribute('aria-label', 'Copied');
buttonElement.setAttribute('title', 'Copied');
}
+ private setCopyError(buttonElement: Element) {
+ this.copyButtonState = 'error';
+ buttonElement.classList.add('content-action--error');
+ buttonElement.innerHTML = '';
+ buttonElement.setAttribute('aria-label', 'Copy failed');
+ buttonElement.setAttribute('title', 'Copy failed');
+ }
+
onCopyButtonMouseLeave(event: Event) {
if (this.copyButtonState === 'idle') {
return;
diff --git a/tools/transforms/content-package/index.ts b/tools/transforms/content-package/index.ts
index 7a870335bd..13d4c85207 100644
--- a/tools/transforms/content-package/index.ts
+++ b/tools/transforms/content-package/index.ts
@@ -4,6 +4,7 @@ import {
computeOutputPathProcessor,
computeWhoUsesProcessor,
extractContentTitleProcessor,
+ outputCleanMarkdownProcessor,
} from './processors';
import { ContentFileReader, contentFileReader } from './readers';
import { cleanMarkdownNunjucksFilter } from './rendering/filters/base64';
@@ -20,6 +21,7 @@ export default new Package('content', [])
.processor(extractContentTitleProcessor)
.processor(computeOutputPathProcessor)
.processor(computeWhoUsesProcessor)
+ .processor(outputCleanMarkdownProcessor)
.config((readFilesProcessor: any, contentFileReader: ContentFileReader) => {
readFilesProcessor.fileReaders.push(contentFileReader);
})
diff --git a/tools/transforms/content-package/processors/index.ts b/tools/transforms/content-package/processors/index.ts
index 4553648936..3684cdebb6 100644
--- a/tools/transforms/content-package/processors/index.ts
+++ b/tools/transforms/content-package/processors/index.ts
@@ -1,3 +1,4 @@
-export * from './extractContentTitle';
export * from './computeOutputPath';
export * from './computeWhoUses';
+export * from './extractContentTitle';
+export * from './outputCleanMarkdown';
diff --git a/tools/transforms/content-package/processors/outputCleanMarkdown.ts b/tools/transforms/content-package/processors/outputCleanMarkdown.ts
new file mode 100644
index 0000000000..19e3fbb517
--- /dev/null
+++ b/tools/transforms/content-package/processors/outputCleanMarkdown.ts
@@ -0,0 +1,61 @@
+import { writeFileSync, mkdirSync } from 'fs';
+import { dirname, join } from 'path';
+import { Processor } from 'dgeni';
+import { Document } from '../../shared';
+
+export class OutputCleanMarkdownProcessor implements Processor {
+ $runAfter = ['readFilesProcessor'];
+ $runBefore = ['renderDocsProcessor'];
+
+ $process(docs: Document[]) {
+ docs.forEach((doc) => {
+ if (doc.docType === 'content' && doc.content) {
+ // Clean the raw markdown content
+ const cleanedMarkdown = this.cleanMarkdown(doc.content);
+
+ // Output to src/assets/content/{id}.md so Angular can serve as assets
+ const outputPath = join('src/assets/content', `${doc.id}.md`);
+
+ // Ensure directory exists
+ mkdirSync(dirname(outputPath), { recursive: true });
+
+ // Write cleaned markdown file
+ writeFileSync(outputPath, cleanedMarkdown, 'utf8');
+ }
+ });
+ }
+
+ private cleanMarkdown(str: string): string {
+ return (
+ str
+ // Fix image sources: /assets/... → https://docs.nestjs.com/assets/...
+ .replace(/src="(\/[^"]+)"/g, 'src="https://docs.nestjs.com$1"')
+ // Fix documentation links: [text](/some/path) → [text](https://docs.nestjs.com/some/path)
+ .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)')
+ // Remove custom Angular components for cleaner portable markdown
+ .replace(/]*><\/app-[^>]*>/g, '')
+ // Convert @@filename to comments with .ts extension
+ .replace(/@@filename\(([^)]+)\)/g, (match, filename) => {
+ // Add .ts extension if filename doesn't already have a proper file extension
+ return filename.match(
+ /\.(ts|js|json|html|css|scss|yaml|yml|xml|dockerfile|md)$/i,
+ )
+ ? `// ${filename}`
+ : `// ${filename}.ts`;
+ })
+ // Remove empty filename placeholders
+ .replace(/@@filename\(\)/g, '')
+ // Split TypeScript/JavaScript versions into separate code blocks
+ .replace(
+ /@@switch\n?/g,
+ '```\n\nJavaScript version:\n\n```javascript\n',
+ )
+ // Remove any empty lines left by component removal
+ .replace(/\n\s*\n\s*\n/g, '\n\n')
+ );
+ }
+}
+
+export function outputCleanMarkdownProcessor() {
+ return new OutputCleanMarkdownProcessor();
+}
diff --git a/tools/transforms/templates/content.template.html b/tools/transforms/templates/content.template.html
index eb5e2d957b..6f3535dd87 100644
--- a/tools/transforms/templates/content.template.html
+++ b/tools/transforms/templates/content.template.html
@@ -1,8 +1,5 @@
{% block content %}
-
- {$ doc.rawContent | cleanMarkdown $}
-